mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-05 11:08:25 +00:00
feat(transport/ble): put the L2CAP PSM in the seam, and implement it for BlueZ
The transport dialled every peer on one configured PSM and bound its own listener to the same one. That works only because BlueZ lets an application choose the PSM it binds, and BlueZ is the exception: Android's listenUsingInsecureL2capChannel and macOS's CBPeripheralManager.publishL2CAPChannel both return an OS-assigned PSM the application cannot request. A dialer cannot guess it, and before a connection exists there is no channel to be told it on other than the advertisement. So the PSM becomes a property of the seam rather than a per-backend assumption. BleIo::listen reports the PSM it actually bound, start_advertising takes the PSM to advertise, and BleScanner yields a ScanAdvert -- address, plus PSM and RSSI when the backend can supply them -- instead of a bare address. The scan/probe loop keeps the learned PSM per address alongside the probe-cooldown map it already maintains and passes it into the existing connect(addr, psm), falling back to the configured PSM when a peer advertises none. The wire layout is a protocol decision and is documented in psm.rs with the byte budget that forces it. A legacy advertising PDU carries 31 bytes; flags take 3 and the 128-bit FIPS service UUID takes 18, which leaves too little for service data keyed on that same 128-bit UUID. The PSM is therefore keyed on the 16-bit UUID 0x9C90, the FIPS UUID's leading 16 bits through the Bluetooth base UUID, costing 6 bytes for a total of 27. The budget is a const assertion, so a change back to a 128-bit key fails the build rather than the radio. It rides the primary advertisement, never the scan response, because a scan response needs an active-scan round trip that drops asymmetrically across chipsets. The BlueZ backend now advertises and reads that service data, which is what lets a BlueZ node tell an Android peer where to dial and learn the peer's OS-assigned PSM in return. Emitting it costs the local_name, which no longer fits the budget. Nothing reads a peer's advertised name -- discovery keys on the service UUID alone, here and on maint -- so dropping it does not affect which nodes can find each other. Compatibility with deployed nodes is unchanged in both directions. BlueZ listeners still bind the configured PSM, so an existing node dialling that PSM still connects. A peer that advertises no service data yields psm: None and is dialled at the configured PSM exactly as before. BleConfig::psm keeps its type, default and meaning; only its doc comment changes to say it is now what to bind and what to dial when a peer advertises nothing. BlueZ shortens a base-range 128-bit UUID to its 16-bit form before building the AD structure, so the service data goes out as the 6-byte AD type 0x16 the layout requires rather than the 20-byte 0x21 form, and the advert stays inside the PDU. Read in BlueZ 5.72: bt_string_to_uuid tests is_base_uuid128 first (lib/uuid.c), and serialize_service_data emits BT_AD_SERVICE_DATA16 for a 2-byte UUID (src/shared/ad.c). This was the open question the change was held on. The BlueZ implementation moves out of io.rs into io_linux.rs at the same time. io.rs now holds only what is platform-neutral -- the traits, ScanAdvert and the mock -- so a new backend is a new io_<platform>.rs beside it rather than another arm inside the shared file. The move is content-preserving: the only changes to the relocated code are three import paths and two rustfmt reflows caused by the dedent. Co-authored-by: Arjen <18398758+Origami74@users.noreply.github.com>
This commit is contained in:
committed by
Johnathan Corgan
co-authored by
Arjen
parent
8ba8076dbb
commit
ae93c90908
@@ -747,8 +747,15 @@ entries become no-ops. Communicates with BlueZ via D-Bus through the
|
|||||||
|
|
||||||
**Advertising and scanning.** When `advertise` is enabled, the transport
|
**Advertising and scanning.** When `advertise` is enabled, the transport
|
||||||
advertises the FIPS service UUID continuously so that nearby nodes can
|
advertises the FIPS service UUID continuously so that nearby nodes can
|
||||||
discover and connect via L2CAP. When `scan` is enabled, the transport
|
discover and connect via L2CAP, plus the L2CAP PSM its listener actually
|
||||||
continuously scans for other FIPS nodes' advertisements. Discovered
|
bound, as a service-data structure (see `src/transport/ble/psm.rs` for the
|
||||||
|
wire layout and why platforms with OS-assigned PSMs need it). The
|
||||||
|
advertisement carries no device name — alongside the PSM a name no longer
|
||||||
|
fits the 31-byte legacy PDU, so the node shows up in generic Bluetooth
|
||||||
|
scanners as an unnamed device with the FIPS UUID. When `scan` is enabled,
|
||||||
|
the transport continuously scans for other FIPS nodes' advertisements and
|
||||||
|
learns each peer's advertised PSM; a peer that advertises none is dialled
|
||||||
|
at the configured `psm`. Discovered
|
||||||
peers are probed immediately (L2CAP connect + pubkey exchange) with a
|
peers are probed immediately (L2CAP connect + pubkey exchange) with a
|
||||||
cooldown (`probe_cooldown_secs`) to prevent rapid re-probing of the same
|
cooldown (`probe_cooldown_secs`) to prevent rapid re-probing of the same
|
||||||
address. If two nodes probe each other at the same time (cross-probe),
|
address. If two nodes probe each other at the same time (cross-probe),
|
||||||
|
|||||||
@@ -711,6 +711,12 @@ pub struct BleConfig {
|
|||||||
pub adapter: Option<String>,
|
pub adapter: Option<String>,
|
||||||
|
|
||||||
/// L2CAP PSM for FIPS connections. Default: 0x0085 (133).
|
/// L2CAP PSM for FIPS connections. Default: 0x0085 (133).
|
||||||
|
///
|
||||||
|
/// This is the PSM to request for this node's listener, and the PSM to
|
||||||
|
/// dial a peer at when that peer advertises none. It is not always the
|
||||||
|
/// PSM finally used: platforms that assign listener PSMs themselves
|
||||||
|
/// report back what they bound and advertise that, and a peer that
|
||||||
|
/// advertises its own PSM is dialled there instead.
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub psm: Option<u16>,
|
pub psm: Option<u16>,
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1164,7 +1164,7 @@ impl Node {
|
|||||||
let transport_id = self.allocate_transport_id();
|
let transport_id = self.allocate_transport_id();
|
||||||
let adapter = ble_config.adapter().to_string();
|
let adapter = ble_config.adapter().to_string();
|
||||||
let mtu = ble_config.mtu();
|
let mtu = ble_config.mtu();
|
||||||
match crate::transport::ble::io::BluerIo::new(&adapter, mtu).await {
|
match crate::transport::ble::io_linux::BluerIo::new(&adapter, mtu).await {
|
||||||
Ok(io) => {
|
Ok(io) => {
|
||||||
let mut ble = crate::transport::ble::BleTransport::new(
|
let mut ble = crate::transport::ble::BleTransport::new(
|
||||||
transport_id,
|
transport_id,
|
||||||
|
|||||||
+139
-406
@@ -1,8 +1,9 @@
|
|||||||
//! BLE I/O abstraction layer.
|
//! BLE I/O abstraction layer.
|
||||||
//!
|
//!
|
||||||
//! Defines the `BleIo` trait that separates transport logic from the
|
//! Defines the `BleIo` seam that separates transport logic from any one
|
||||||
//! BlueZ/bluer stack. `BluerIo` (behind `cfg(bluer_available)`) provides
|
//! radio stack, and the in-memory `MockBleIo` test double. Everything in
|
||||||
//! the real implementation; `MockBleIo` provides an in-memory test double.
|
//! this file is platform-neutral; each concrete backend lives beside it in
|
||||||
|
//! its own `io_<platform>.rs` — [`super::io_linux`] for BlueZ.
|
||||||
|
|
||||||
use crate::transport::TransportError;
|
use crate::transport::TransportError;
|
||||||
|
|
||||||
@@ -58,12 +59,53 @@ pub trait BleAcceptor: Send {
|
|||||||
) -> impl std::future::Future<Output = Result<Self::Stream, TransportError>> + Send;
|
) -> impl std::future::Future<Output = Result<Self::Stream, TransportError>> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One advertisement observed by a scanner.
|
||||||
|
///
|
||||||
|
/// Carries what the backend could read from the advert, not what it wishes
|
||||||
|
/// were there: a backend that cannot surface a field reports `None` for it,
|
||||||
|
/// the way `TransportHandle::local_addr` already does for transports that
|
||||||
|
/// have no address to give.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct ScanAdvert {
|
||||||
|
/// The advertiser's link address.
|
||||||
|
pub addr: BleAddr,
|
||||||
|
/// The L2CAP listener PSM the peer advertised, if it advertised one.
|
||||||
|
///
|
||||||
|
/// `None` for a legacy UUID-only advertiser, and for a backend that
|
||||||
|
/// cannot read advertised service data. The dialer then falls back to the
|
||||||
|
/// configured PSM. See [`super::psm`] for the wire layout.
|
||||||
|
pub psm: Option<u16>,
|
||||||
|
/// Received signal strength in dBm, if the backend reports it.
|
||||||
|
pub rssi: Option<i16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScanAdvert {
|
||||||
|
/// An advert carrying nothing but a link address — what a legacy
|
||||||
|
/// UUID-only advertiser produces.
|
||||||
|
pub fn new(addr: BleAddr) -> Self {
|
||||||
|
Self {
|
||||||
|
addr,
|
||||||
|
psm: None,
|
||||||
|
rssi: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An advert carrying a listener PSM.
|
||||||
|
pub fn with_psm(addr: BleAddr, psm: u16) -> Self {
|
||||||
|
Self {
|
||||||
|
addr,
|
||||||
|
psm: Some(psm),
|
||||||
|
rssi: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A scanner that yields discovered BLE devices advertising the FIPS UUID.
|
/// A scanner that yields discovered BLE devices advertising the FIPS UUID.
|
||||||
pub trait BleScanner: Send {
|
pub trait BleScanner: Send {
|
||||||
/// Wait for the next discovered device.
|
/// Wait for the next observed advertisement.
|
||||||
///
|
///
|
||||||
/// Returns `None` when scanning is stopped.
|
/// Returns `None` when scanning is stopped.
|
||||||
fn next(&mut self) -> impl std::future::Future<Output = Option<BleAddr>> + Send;
|
fn next(&mut self) -> impl std::future::Future<Output = Option<ScanAdvert>> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Core BLE I/O operations.
|
/// Core BLE I/O operations.
|
||||||
@@ -79,11 +121,19 @@ pub trait BleIo: Send + Sync + 'static {
|
|||||||
/// The concrete scanner type.
|
/// The concrete scanner type.
|
||||||
type Scanner: BleScanner + 'static;
|
type Scanner: BleScanner + 'static;
|
||||||
|
|
||||||
/// Start listening for inbound L2CAP connections on the given PSM.
|
/// Start listening for inbound L2CAP connections, and report the PSM
|
||||||
|
/// actually bound.
|
||||||
|
///
|
||||||
|
/// `psm` is the PSM to request. Backends that let an application choose
|
||||||
|
/// one (BlueZ) bind it and report it back unchanged. Backends whose
|
||||||
|
/// platform assigns the PSM (Android, macOS) ignore the request and
|
||||||
|
/// report what the OS gave them — which is why this returns a value
|
||||||
|
/// rather than being assumed equal to the argument. The reported PSM is
|
||||||
|
/// what gets advertised.
|
||||||
fn listen(
|
fn listen(
|
||||||
&self,
|
&self,
|
||||||
psm: u16,
|
psm: u16,
|
||||||
) -> impl std::future::Future<Output = Result<Self::Acceptor, TransportError>> + Send;
|
) -> impl std::future::Future<Output = Result<(Self::Acceptor, u16), TransportError>> + Send;
|
||||||
|
|
||||||
/// Connect to a remote BLE device on the given PSM.
|
/// Connect to a remote BLE device on the given PSM.
|
||||||
fn connect(
|
fn connect(
|
||||||
@@ -92,9 +142,15 @@ pub trait BleIo: Send + Sync + 'static {
|
|||||||
psm: u16,
|
psm: u16,
|
||||||
) -> impl std::future::Future<Output = Result<Self::Stream, TransportError>> + Send;
|
) -> impl std::future::Future<Output = Result<Self::Stream, TransportError>> + Send;
|
||||||
|
|
||||||
/// Start advertising the FIPS service UUID.
|
/// Start advertising the FIPS service UUID, and the listener PSM.
|
||||||
|
///
|
||||||
|
/// `psm` is the PSM this node's listener is bound to; see [`super::psm`]
|
||||||
|
/// for the wire layout it should be advertised in. A backend that cannot
|
||||||
|
/// put it in its advert ignores the argument, and peers dial it at their
|
||||||
|
/// configured PSM as before.
|
||||||
fn start_advertising(
|
fn start_advertising(
|
||||||
&self,
|
&self,
|
||||||
|
psm: u16,
|
||||||
) -> impl std::future::Future<Output = Result<(), TransportError>> + Send;
|
) -> impl std::future::Future<Output = Result<(), TransportError>> + Send;
|
||||||
|
|
||||||
/// Stop advertising.
|
/// Stop advertising.
|
||||||
@@ -114,390 +170,6 @@ pub trait BleIo: Send + Sync + 'static {
|
|||||||
fn adapter_name(&self) -> &str;
|
fn adapter_name(&self) -> &str;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// BluerIo — Production BLE I/O via BlueZ D-Bus
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
#[cfg(bluer_available)]
|
|
||||||
mod bluer_impl {
|
|
||||||
use super::*;
|
|
||||||
use crate::transport::TransportError;
|
|
||||||
|
|
||||||
use bluer::l2cap::{SeqPacket, SeqPacketListener, Socket, SocketAddr};
|
|
||||||
use bluer::{
|
|
||||||
AdapterEvent, AddressType, DiscoveryFilter, DiscoveryTransport, adv::Advertisement,
|
|
||||||
};
|
|
||||||
use futures::StreamExt;
|
|
||||||
use std::collections::{BTreeSet, HashSet};
|
|
||||||
use std::pin::Pin;
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
use tracing::{debug, trace};
|
|
||||||
|
|
||||||
/// FIPS BLE service UUID.
|
|
||||||
///
|
|
||||||
/// Derived from SHA-256("FIPS: welcome to cryptoanarchy") with UUID v4
|
|
||||||
/// version/variant bits applied.
|
|
||||||
pub const FIPS_SERVICE_UUID: bluer::Uuid =
|
|
||||||
bluer::Uuid::from_u128(0x9c90_b790_2cc5_42c0_9f87_c9cc_4064_8f4c);
|
|
||||||
|
|
||||||
/// Map a bluer error to a TransportError.
|
|
||||||
fn map_err(context: &str, e: bluer::Error) -> TransportError {
|
|
||||||
TransportError::Io(std::io::Error::other(format!("{}: {}", context, e)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Map a std::io::Error to a TransportError.
|
|
||||||
fn map_io_err(context: &str, e: std::io::Error) -> TransportError {
|
|
||||||
TransportError::Io(std::io::Error::new(e.kind(), format!("{}: {}", context, e)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
|
||||||
// BluerStream
|
|
||||||
// ----------------------------------------------------------------
|
|
||||||
|
|
||||||
/// BLE stream wrapping a bluer L2CAP SeqPacket connection.
|
|
||||||
pub struct BluerStream {
|
|
||||||
conn: SeqPacket,
|
|
||||||
remote: BleAddr,
|
|
||||||
send_mtu: u16,
|
|
||||||
recv_mtu: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BluerStream {
|
|
||||||
/// Construct from a connected SeqPacket, querying MTU values.
|
|
||||||
pub fn new(conn: SeqPacket, remote: BleAddr) -> Result<Self, TransportError> {
|
|
||||||
let send_mtu = conn.send_mtu().map_err(|e| map_io_err("send_mtu", e))? as u16;
|
|
||||||
let recv_mtu = conn.recv_mtu().map_err(|e| map_io_err("recv_mtu", e))? as u16;
|
|
||||||
|
|
||||||
// Log negotiated PHY for diagnostics (2M vs 1M)
|
|
||||||
match conn.as_ref().phy() {
|
|
||||||
Ok(phy) => {
|
|
||||||
debug!(addr = %remote, phy, send_mtu, recv_mtu, "BLE connection established")
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
debug!(addr = %remote, send_mtu, recv_mtu, "BLE connection established (PHY query unsupported)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
conn,
|
|
||||||
remote,
|
|
||||||
send_mtu,
|
|
||||||
recv_mtu,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BleStream for BluerStream {
|
|
||||||
async fn send(&self, data: &[u8]) -> Result<(), TransportError> {
|
|
||||||
self.conn
|
|
||||||
.send(data)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|e| TransportError::SendFailed(format!("{}", e)))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn recv(&self, buf: &mut [u8]) -> Result<usize, TransportError> {
|
|
||||||
self.conn
|
|
||||||
.recv(buf)
|
|
||||||
.await
|
|
||||||
.map_err(|e| TransportError::RecvFailed(format!("{}", e)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_mtu(&self) -> u16 {
|
|
||||||
self.send_mtu
|
|
||||||
}
|
|
||||||
|
|
||||||
fn recv_mtu(&self) -> u16 {
|
|
||||||
self.recv_mtu
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remote_addr(&self) -> &BleAddr {
|
|
||||||
&self.remote
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
|
||||||
// BluerAcceptor
|
|
||||||
// ----------------------------------------------------------------
|
|
||||||
|
|
||||||
/// Acceptor wrapping a bluer L2CAP SeqPacketListener.
|
|
||||||
pub struct BluerAcceptor {
|
|
||||||
listener: SeqPacketListener,
|
|
||||||
adapter_name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BleAcceptor for BluerAcceptor {
|
|
||||||
type Stream = BluerStream;
|
|
||||||
|
|
||||||
async fn accept(&mut self) -> Result<BluerStream, TransportError> {
|
|
||||||
let (conn, peer_sa) = self
|
|
||||||
.listener
|
|
||||||
.accept()
|
|
||||||
.await
|
|
||||||
.map_err(|e| map_io_err("accept", e))?;
|
|
||||||
|
|
||||||
let remote = BleAddr::from_bluer(peer_sa.addr, &self.adapter_name);
|
|
||||||
BluerStream::new(conn, remote)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
|
||||||
// BluerScanner
|
|
||||||
// ----------------------------------------------------------------
|
|
||||||
|
|
||||||
/// Scanner wrapping a bluer discovery event stream.
|
|
||||||
pub struct BluerScanner {
|
|
||||||
events: Pin<Box<dyn futures::Stream<Item = AdapterEvent> + Send>>,
|
|
||||||
adapter: bluer::Adapter,
|
|
||||||
adapter_name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BleScanner for BluerScanner {
|
|
||||||
async fn next(&mut self) -> Option<BleAddr> {
|
|
||||||
loop {
|
|
||||||
match self.events.next().await {
|
|
||||||
Some(AdapterEvent::DeviceAdded(addr)) => {
|
|
||||||
// Check if device advertises FIPS UUID
|
|
||||||
if let Ok(device) = self.adapter.device(addr) {
|
|
||||||
match device.uuids().await {
|
|
||||||
Ok(Some(uuids)) if uuids.contains(&FIPS_SERVICE_UUID) => {
|
|
||||||
let ble_addr = BleAddr::from_bluer(addr, &self.adapter_name);
|
|
||||||
debug!(addr = %ble_addr, "BLE scanner: FIPS peer found");
|
|
||||||
return Some(ble_addr);
|
|
||||||
}
|
|
||||||
Ok(_) => {
|
|
||||||
trace!(addr = %addr, "BLE scanner: device without FIPS UUID");
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
trace!(addr = %addr, error = %e, "BLE scanner: failed to read UUIDs");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(_) => continue,
|
|
||||||
None => return None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
|
||||||
// BluerIo
|
|
||||||
// ----------------------------------------------------------------
|
|
||||||
|
|
||||||
/// Production BLE I/O implementation via BlueZ D-Bus (bluer crate).
|
|
||||||
pub struct BluerIo {
|
|
||||||
#[allow(dead_code)] // Session must be kept alive for the adapter.
|
|
||||||
session: bluer::Session,
|
|
||||||
adapter: bluer::Adapter,
|
|
||||||
adapter_name: String,
|
|
||||||
adv_handle: Mutex<Option<bluer::adv::AdvertisementHandle>>,
|
|
||||||
mtu: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BluerIo {
|
|
||||||
/// Create a new BluerIo for the given adapter.
|
|
||||||
///
|
|
||||||
/// Connects to BlueZ via D-Bus and powers on the adapter.
|
|
||||||
pub async fn new(adapter_name: &str, mtu: u16) -> Result<Self, TransportError> {
|
|
||||||
let session = bluer::Session::new()
|
|
||||||
.await
|
|
||||||
.map_err(|e| map_err("Session::new", e))?;
|
|
||||||
|
|
||||||
let adapter = if adapter_name == "default" {
|
|
||||||
session
|
|
||||||
.default_adapter()
|
|
||||||
.await
|
|
||||||
.map_err(|e| map_err("default_adapter", e))?
|
|
||||||
} else {
|
|
||||||
session
|
|
||||||
.adapter(adapter_name)
|
|
||||||
.map_err(|e| map_err("adapter", e))?
|
|
||||||
};
|
|
||||||
|
|
||||||
adapter
|
|
||||||
.set_powered(true)
|
|
||||||
.await
|
|
||||||
.map_err(|e| map_err("set_powered", e))?;
|
|
||||||
|
|
||||||
let name = adapter.name().to_string();
|
|
||||||
debug!(adapter = %name, "BluerIo initialized");
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
session,
|
|
||||||
adapter,
|
|
||||||
adapter_name: name,
|
|
||||||
adv_handle: Mutex::new(None),
|
|
||||||
mtu,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BleIo for BluerIo {
|
|
||||||
type Stream = BluerStream;
|
|
||||||
type Acceptor = BluerAcceptor;
|
|
||||||
type Scanner = BluerScanner;
|
|
||||||
|
|
||||||
async fn listen(&self, psm: u16) -> Result<Self::Acceptor, TransportError> {
|
|
||||||
let local_addr = self
|
|
||||||
.adapter
|
|
||||||
.address()
|
|
||||||
.await
|
|
||||||
.map_err(|e| map_err("address", e))?;
|
|
||||||
|
|
||||||
let sa = SocketAddr::new(local_addr, AddressType::LePublic, psm);
|
|
||||||
let listener = SeqPacketListener::bind(sa)
|
|
||||||
.await
|
|
||||||
.map_err(|e| map_io_err("bind", e))?;
|
|
||||||
|
|
||||||
// Request high MTU for accepted connections
|
|
||||||
listener
|
|
||||||
.as_ref()
|
|
||||||
.set_recv_mtu(self.mtu)
|
|
||||||
.map_err(|e| map_io_err("set_recv_mtu", e))?;
|
|
||||||
|
|
||||||
// Prevent sniff mode to reduce latency during data transfer
|
|
||||||
if let Err(e) = listener.as_ref().set_power_forced_active(true) {
|
|
||||||
debug!(error = %e, "BLE listener: set_power_forced_active not supported");
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!(psm, mtu = self.mtu, "BLE listener bound");
|
|
||||||
|
|
||||||
Ok(BluerAcceptor {
|
|
||||||
listener,
|
|
||||||
adapter_name: self.adapter_name.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn connect(&self, addr: &BleAddr, psm: u16) -> Result<Self::Stream, TransportError> {
|
|
||||||
let target_sa = addr.to_socket_addr(psm);
|
|
||||||
|
|
||||||
let socket = Socket::<SeqPacket>::new_seq_packet()
|
|
||||||
.map_err(|e| map_io_err("new_seq_packet", e))?;
|
|
||||||
socket
|
|
||||||
.bind(SocketAddr::any_le())
|
|
||||||
.map_err(|e| map_io_err("bind", e))?;
|
|
||||||
socket
|
|
||||||
.set_recv_mtu(self.mtu)
|
|
||||||
.map_err(|e| map_io_err("set_recv_mtu", e))?;
|
|
||||||
|
|
||||||
// Prevent sniff mode to reduce latency during data transfer
|
|
||||||
if let Err(e) = socket.set_power_forced_active(true) {
|
|
||||||
debug!(error = %e, "BLE connect: set_power_forced_active not supported");
|
|
||||||
}
|
|
||||||
|
|
||||||
let conn = socket
|
|
||||||
.connect(target_sa)
|
|
||||||
.await
|
|
||||||
.map_err(|e| map_io_err("connect", e))?;
|
|
||||||
|
|
||||||
let remote = addr.clone();
|
|
||||||
BluerStream::new(conn, remote)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn start_advertising(&self) -> Result<(), TransportError> {
|
|
||||||
let adv = Advertisement {
|
|
||||||
advertisement_type: bluer::adv::Type::Peripheral,
|
|
||||||
service_uuids: {
|
|
||||||
let mut s = BTreeSet::new();
|
|
||||||
s.insert(FIPS_SERVICE_UUID);
|
|
||||||
s
|
|
||||||
},
|
|
||||||
local_name: Some("fips".to_string()),
|
|
||||||
min_interval: Some(std::time::Duration::from_millis(400)),
|
|
||||||
max_interval: Some(std::time::Duration::from_millis(600)),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let handle = self
|
|
||||||
.adapter
|
|
||||||
.advertise(adv)
|
|
||||||
.await
|
|
||||||
.map_err(|e| map_err("advertise", e))?;
|
|
||||||
|
|
||||||
*self.adv_handle.lock().await = Some(handle);
|
|
||||||
debug!("BLE advertising started");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn stop_advertising(&self) -> Result<(), TransportError> {
|
|
||||||
let _ = self.adv_handle.lock().await.take();
|
|
||||||
debug!("BLE advertising stopped");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn start_scanning(&self) -> Result<Self::Scanner, TransportError> {
|
|
||||||
// Clear cached devices so BlueZ fires DeviceAdded for every
|
|
||||||
// advertisement. Without this, already-known devices only
|
|
||||||
// produce PropertyChanged events (which bluer doesn't expose
|
|
||||||
// at the device level), causing the scanner to miss peers
|
|
||||||
// after a daemon restart.
|
|
||||||
if let Ok(cached) = self.adapter.device_addresses().await {
|
|
||||||
let count = cached.len();
|
|
||||||
for addr in cached {
|
|
||||||
let _ = self.adapter.remove_device(addr).await;
|
|
||||||
}
|
|
||||||
if count > 0 {
|
|
||||||
debug!(count, "BLE scanner: cleared cached devices");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set discovery filter for LE transport with FIPS UUID
|
|
||||||
let filter = DiscoveryFilter {
|
|
||||||
transport: DiscoveryTransport::Le,
|
|
||||||
uuids: {
|
|
||||||
let mut s = HashSet::new();
|
|
||||||
s.insert(FIPS_SERVICE_UUID);
|
|
||||||
s
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
self.adapter
|
|
||||||
.set_discovery_filter(filter)
|
|
||||||
.await
|
|
||||||
.map_err(|e| map_err("set_discovery_filter", e))?;
|
|
||||||
|
|
||||||
let events = self
|
|
||||||
.adapter
|
|
||||||
.discover_devices()
|
|
||||||
.await
|
|
||||||
.map_err(|e| map_err("discover_devices", e))?;
|
|
||||||
|
|
||||||
debug!("BLE scanning started");
|
|
||||||
|
|
||||||
Ok(BluerScanner {
|
|
||||||
events: Box::pin(events),
|
|
||||||
adapter: self.adapter.clone(),
|
|
||||||
adapter_name: self.adapter_name.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn local_addr(&self) -> Result<BleAddr, TransportError> {
|
|
||||||
// Use futures::executor::block_on since this is a sync method
|
|
||||||
// but needs an async call. The adapter address is cached so
|
|
||||||
// the D-Bus call is fast.
|
|
||||||
let addr = futures::executor::block_on(self.adapter.address())
|
|
||||||
.map_err(|e| map_err("address", e))?;
|
|
||||||
Ok(BleAddr::from_bluer(addr, &self.adapter_name))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn adapter_name(&self) -> &str {
|
|
||||||
&self.adapter_name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile-time assertion that BluerIo satisfies Send + Sync.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn _assert_bluer_io_send_sync() {
|
|
||||||
fn require<T: Send + Sync>() {}
|
|
||||||
require::<BluerIo>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(bluer_available)]
|
|
||||||
pub use bluer_impl::{BluerAcceptor, BluerIo, BluerScanner, BluerStream, FIPS_SERVICE_UUID};
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Mock BLE I/O (for testing without hardware)
|
// Mock BLE I/O (for testing without hardware)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -583,13 +255,13 @@ impl BleAcceptor for MockBleAcceptor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mock BLE scanner backed by a channel of discovered addresses.
|
/// Mock BLE scanner backed by a channel of observed adverts.
|
||||||
pub struct MockBleScanner {
|
pub struct MockBleScanner {
|
||||||
rx: tokio::sync::mpsc::Receiver<BleAddr>,
|
rx: tokio::sync::mpsc::Receiver<ScanAdvert>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BleScanner for MockBleScanner {
|
impl BleScanner for MockBleScanner {
|
||||||
async fn next(&mut self) -> Option<BleAddr> {
|
async fn next(&mut self) -> Option<ScanAdvert> {
|
||||||
self.rx.recv().await
|
self.rx.recv().await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -607,9 +279,15 @@ pub struct MockBleIo {
|
|||||||
local_addr: BleAddr,
|
local_addr: BleAddr,
|
||||||
accept_tx: tokio::sync::mpsc::Sender<MockBleStream>,
|
accept_tx: tokio::sync::mpsc::Sender<MockBleStream>,
|
||||||
accept_rx: std::sync::Mutex<Option<tokio::sync::mpsc::Receiver<MockBleStream>>>,
|
accept_rx: std::sync::Mutex<Option<tokio::sync::mpsc::Receiver<MockBleStream>>>,
|
||||||
scan_tx: tokio::sync::mpsc::Sender<BleAddr>,
|
scan_tx: tokio::sync::mpsc::Sender<ScanAdvert>,
|
||||||
scan_rx: std::sync::Mutex<Option<tokio::sync::mpsc::Receiver<BleAddr>>>,
|
scan_rx: std::sync::Mutex<Option<tokio::sync::mpsc::Receiver<ScanAdvert>>>,
|
||||||
connect_handler: std::sync::Mutex<Option<ConnectHandler>>,
|
connect_handler: std::sync::Mutex<Option<ConnectHandler>>,
|
||||||
|
/// PSM `listen` reports back, overriding the requested one.
|
||||||
|
///
|
||||||
|
/// Simulates a platform that assigns the PSM itself.
|
||||||
|
bound_psm: std::sync::Mutex<Option<u16>>,
|
||||||
|
/// PSM most recently passed to `start_advertising`.
|
||||||
|
advertised_psm: std::sync::Mutex<Option<u16>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MockBleIo {
|
impl MockBleIo {
|
||||||
@@ -625,6 +303,8 @@ impl MockBleIo {
|
|||||||
scan_tx,
|
scan_tx,
|
||||||
scan_rx: std::sync::Mutex::new(Some(scan_rx)),
|
scan_rx: std::sync::Mutex::new(Some(scan_rx)),
|
||||||
connect_handler: std::sync::Mutex::new(None),
|
connect_handler: std::sync::Mutex::new(None),
|
||||||
|
bound_psm: std::sync::Mutex::new(None),
|
||||||
|
advertised_psm: std::sync::Mutex::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -633,9 +313,29 @@ impl MockBleIo {
|
|||||||
let _ = self.accept_tx.send(stream).await;
|
let _ = self.accept_tx.send(stream).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inject a scan result (simulates discovering a remote device).
|
/// Inject a scan result (simulates discovering a legacy UUID-only
|
||||||
|
/// advertiser, which carries no PSM).
|
||||||
pub async fn inject_scan_result(&self, addr: BleAddr) {
|
pub async fn inject_scan_result(&self, addr: BleAddr) {
|
||||||
let _ = self.scan_tx.send(addr).await;
|
self.inject_scan_advert(ScanAdvert::new(addr)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inject an observed advertisement verbatim.
|
||||||
|
pub async fn inject_scan_advert(&self, advert: ScanAdvert) {
|
||||||
|
let _ = self.scan_tx.send(advert).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make `listen` report a PSM other than the one requested, the way a
|
||||||
|
/// platform that assigns PSMs itself would.
|
||||||
|
pub fn set_bound_psm(&self, psm: u16) {
|
||||||
|
*self.bound_psm.lock().unwrap_or_else(|e| e.into_inner()) = Some(psm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The PSM most recently handed to `start_advertising`.
|
||||||
|
pub fn advertised_psm(&self) -> Option<u16> {
|
||||||
|
*self
|
||||||
|
.advertised_psm
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|e| e.into_inner())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set a handler for outbound connect calls.
|
/// Set a handler for outbound connect calls.
|
||||||
@@ -655,14 +355,19 @@ impl BleIo for MockBleIo {
|
|||||||
type Acceptor = MockBleAcceptor;
|
type Acceptor = MockBleAcceptor;
|
||||||
type Scanner = MockBleScanner;
|
type Scanner = MockBleScanner;
|
||||||
|
|
||||||
async fn listen(&self, _psm: u16) -> Result<Self::Acceptor, TransportError> {
|
async fn listen(&self, psm: u16) -> Result<(Self::Acceptor, u16), TransportError> {
|
||||||
let rx = self
|
let rx = self
|
||||||
.accept_rx
|
.accept_rx
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.take()
|
.take()
|
||||||
.ok_or_else(|| TransportError::NotSupported("acceptor already taken".into()))?;
|
.ok_or_else(|| TransportError::NotSupported("acceptor already taken".into()))?;
|
||||||
Ok(MockBleAcceptor { rx })
|
let bound = self
|
||||||
|
.bound_psm
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|e| e.into_inner())
|
||||||
|
.unwrap_or(psm);
|
||||||
|
Ok((MockBleAcceptor { rx }, bound))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn connect(&self, addr: &BleAddr, psm: u16) -> Result<Self::Stream, TransportError> {
|
async fn connect(&self, addr: &BleAddr, psm: u16) -> Result<Self::Stream, TransportError> {
|
||||||
@@ -676,7 +381,11 @@ impl BleIo for MockBleIo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn start_advertising(&self) -> Result<(), TransportError> {
|
async fn start_advertising(&self, psm: u16) -> Result<(), TransportError> {
|
||||||
|
*self
|
||||||
|
.advertised_psm
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|e| e.into_inner()) = Some(psm);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -751,7 +460,8 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_mock_io_listen_accept() {
|
async fn test_mock_io_listen_accept() {
|
||||||
let io = MockBleIo::new("hci0", test_addr(1));
|
let io = MockBleIo::new("hci0", test_addr(1));
|
||||||
let mut acceptor = io.listen(0x0085).await.unwrap();
|
let (mut acceptor, bound) = io.listen(0x0085).await.unwrap();
|
||||||
|
assert_eq!(bound, 0x0085, "mock binds what it is asked for by default");
|
||||||
|
|
||||||
let (stream_a, _stream_b) = MockBleStream::pair(test_addr(1), test_addr(2), 2048);
|
let (stream_a, _stream_b) = MockBleStream::pair(test_addr(1), test_addr(2), 2048);
|
||||||
io.inject_inbound(stream_a).await;
|
io.inject_inbound(stream_a).await;
|
||||||
@@ -789,8 +499,8 @@ mod tests {
|
|||||||
io.inject_scan_result(test_addr(2)).await;
|
io.inject_scan_result(test_addr(2)).await;
|
||||||
io.inject_scan_result(test_addr(3)).await;
|
io.inject_scan_result(test_addr(3)).await;
|
||||||
|
|
||||||
assert_eq!(scanner.next().await, Some(test_addr(2)));
|
assert_eq!(scanner.next().await, Some(ScanAdvert::new(test_addr(2))));
|
||||||
assert_eq!(scanner.next().await, Some(test_addr(3)));
|
assert_eq!(scanner.next().await, Some(ScanAdvert::new(test_addr(3))));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -803,10 +513,33 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_mock_io_advertising_noop() {
|
async fn test_mock_io_advertising_noop() {
|
||||||
let io = MockBleIo::new("hci0", test_addr(1));
|
let io = MockBleIo::new("hci0", test_addr(1));
|
||||||
io.start_advertising().await.unwrap();
|
io.start_advertising(0x0085).await.unwrap();
|
||||||
|
assert_eq!(io.advertised_psm(), Some(0x0085));
|
||||||
io.stop_advertising().await.unwrap();
|
io.stop_advertising().await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A backend whose platform assigns the PSM reports back something other
|
||||||
|
/// than what was requested — the case the return value exists for.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_mock_io_listen_reports_an_os_assigned_psm() {
|
||||||
|
let io = MockBleIo::new("hci0", test_addr(1));
|
||||||
|
io.set_bound_psm(0x00C1);
|
||||||
|
let (_acceptor, bound) = io.listen(0x0085).await.unwrap();
|
||||||
|
assert_eq!(bound, 0x00C1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_mock_io_scan_advert_carries_a_psm() {
|
||||||
|
let io = MockBleIo::new("hci0", test_addr(1));
|
||||||
|
let mut scanner = io.start_scanning().await.unwrap();
|
||||||
|
io.inject_scan_advert(ScanAdvert::with_psm(test_addr(2), 0x00C1))
|
||||||
|
.await;
|
||||||
|
let advert = scanner.next().await.unwrap();
|
||||||
|
assert_eq!(advert.addr, test_addr(2));
|
||||||
|
assert_eq!(advert.psm, Some(0x00C1));
|
||||||
|
assert_eq!(advert.rssi, None);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_mock_io_listen_twice_fails() {
|
async fn test_mock_io_listen_twice_fails() {
|
||||||
let io = MockBleIo::new("hci0", test_addr(1));
|
let io = MockBleIo::new("hci0", test_addr(1));
|
||||||
|
|||||||
@@ -0,0 +1,456 @@
|
|||||||
|
//! BlueZ backend for the BLE transport.
|
||||||
|
//!
|
||||||
|
//! The Linux implementation of the [`BleIo`](super::io::BleIo) seam, over the
|
||||||
|
//! `bluer` crate's D-Bus binding to BlueZ. Everything platform-neutral lives
|
||||||
|
//! in [`super::io`] and the modules beside it; this file holds only what
|
||||||
|
//! speaks to BlueZ.
|
||||||
|
|
||||||
|
use super::io::*;
|
||||||
|
use crate::transport::TransportError;
|
||||||
|
|
||||||
|
use bluer::l2cap::{SeqPacket, SeqPacketListener, Socket, SocketAddr};
|
||||||
|
use bluer::{AdapterEvent, AddressType, DiscoveryFilter, DiscoveryTransport, adv::Advertisement};
|
||||||
|
use futures::StreamExt;
|
||||||
|
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
||||||
|
use std::pin::Pin;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use tracing::{debug, trace};
|
||||||
|
|
||||||
|
use super::addr::BleAddr;
|
||||||
|
use super::psm;
|
||||||
|
|
||||||
|
/// FIPS BLE service UUID.
|
||||||
|
///
|
||||||
|
/// Derived from SHA-256("FIPS: welcome to cryptoanarchy") with UUID v4
|
||||||
|
/// version/variant bits applied.
|
||||||
|
pub const FIPS_SERVICE_UUID: bluer::Uuid =
|
||||||
|
bluer::Uuid::from_u128(0x9c90_b790_2cc5_42c0_9f87_c9cc_4064_8f4c);
|
||||||
|
|
||||||
|
/// The PSM service-data key as a whole UUID.
|
||||||
|
///
|
||||||
|
/// BlueZ speaks in full UUIDs, so [`psm::PSM_SERVICE_DATA_UUID16`] is
|
||||||
|
/// expanded through the Bluetooth base UUID
|
||||||
|
/// (`00009C90-0000-1000-8000-00805F9B34FB`). The controller emits it
|
||||||
|
/// back on the air as the 16-bit Service Data AD structure the wire
|
||||||
|
/// layout in [`psm`] specifies.
|
||||||
|
pub const PSM_SERVICE_DATA_UUID: bluer::Uuid = bluer::Uuid::from_u128(
|
||||||
|
((psm::PSM_SERVICE_DATA_UUID16 as u128) << 96) | 0x0000_0000_0000_1000_8000_0080_5F9B_34FB,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Map a bluer error to a TransportError.
|
||||||
|
fn map_err(context: &str, e: bluer::Error) -> TransportError {
|
||||||
|
TransportError::Io(std::io::Error::other(format!("{}: {}", context, e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map a std::io::Error to a TransportError.
|
||||||
|
fn map_io_err(context: &str, e: std::io::Error) -> TransportError {
|
||||||
|
TransportError::Io(std::io::Error::new(e.kind(), format!("{}: {}", context, e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// BluerStream
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
/// BLE stream wrapping a bluer L2CAP SeqPacket connection.
|
||||||
|
pub struct BluerStream {
|
||||||
|
conn: SeqPacket,
|
||||||
|
remote: BleAddr,
|
||||||
|
send_mtu: u16,
|
||||||
|
recv_mtu: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BluerStream {
|
||||||
|
/// Construct from a connected SeqPacket, querying MTU values.
|
||||||
|
pub fn new(conn: SeqPacket, remote: BleAddr) -> Result<Self, TransportError> {
|
||||||
|
let send_mtu = conn.send_mtu().map_err(|e| map_io_err("send_mtu", e))? as u16;
|
||||||
|
let recv_mtu = conn.recv_mtu().map_err(|e| map_io_err("recv_mtu", e))? as u16;
|
||||||
|
|
||||||
|
// Log negotiated PHY for diagnostics (2M vs 1M)
|
||||||
|
match conn.as_ref().phy() {
|
||||||
|
Ok(phy) => {
|
||||||
|
debug!(addr = %remote, phy, send_mtu, recv_mtu, "BLE connection established")
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
debug!(addr = %remote, send_mtu, recv_mtu, "BLE connection established (PHY query unsupported)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
conn,
|
||||||
|
remote,
|
||||||
|
send_mtu,
|
||||||
|
recv_mtu,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BleStream for BluerStream {
|
||||||
|
async fn send(&self, data: &[u8]) -> Result<(), TransportError> {
|
||||||
|
self.conn
|
||||||
|
.send(data)
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|e| TransportError::SendFailed(format!("{}", e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn recv(&self, buf: &mut [u8]) -> Result<usize, TransportError> {
|
||||||
|
self.conn
|
||||||
|
.recv(buf)
|
||||||
|
.await
|
||||||
|
.map_err(|e| TransportError::RecvFailed(format!("{}", e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_mtu(&self) -> u16 {
|
||||||
|
self.send_mtu
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recv_mtu(&self) -> u16 {
|
||||||
|
self.recv_mtu
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remote_addr(&self) -> &BleAddr {
|
||||||
|
&self.remote
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// BluerAcceptor
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Acceptor wrapping a bluer L2CAP SeqPacketListener.
|
||||||
|
pub struct BluerAcceptor {
|
||||||
|
listener: SeqPacketListener,
|
||||||
|
adapter_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BleAcceptor for BluerAcceptor {
|
||||||
|
type Stream = BluerStream;
|
||||||
|
|
||||||
|
async fn accept(&mut self) -> Result<BluerStream, TransportError> {
|
||||||
|
let (conn, peer_sa) = self
|
||||||
|
.listener
|
||||||
|
.accept()
|
||||||
|
.await
|
||||||
|
.map_err(|e| map_io_err("accept", e))?;
|
||||||
|
|
||||||
|
let remote = BleAddr::from_bluer(peer_sa.addr, &self.adapter_name);
|
||||||
|
BluerStream::new(conn, remote)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// BluerScanner
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Scanner wrapping a bluer discovery event stream.
|
||||||
|
pub struct BluerScanner {
|
||||||
|
events: Pin<Box<dyn futures::Stream<Item = AdapterEvent> + Send>>,
|
||||||
|
adapter: bluer::Adapter,
|
||||||
|
adapter_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BleScanner for BluerScanner {
|
||||||
|
/// Yields adverts with the PSM and RSSI when BlueZ can supply them.
|
||||||
|
///
|
||||||
|
/// The PSM comes out of the peer's Service Data AD structure (see
|
||||||
|
/// `super::super::psm`); a peer that advertises none — a legacy
|
||||||
|
/// UUID-only advertiser — yields `psm: None` and is dialled at the
|
||||||
|
/// configured PSM, exactly as before.
|
||||||
|
async fn next(&mut self) -> Option<ScanAdvert> {
|
||||||
|
loop {
|
||||||
|
match self.events.next().await {
|
||||||
|
Some(AdapterEvent::DeviceAdded(addr)) => {
|
||||||
|
// Check if device advertises FIPS UUID
|
||||||
|
if let Ok(device) = self.adapter.device(addr) {
|
||||||
|
match device.uuids().await {
|
||||||
|
Ok(Some(uuids)) if uuids.contains(&FIPS_SERVICE_UUID) => {
|
||||||
|
let ble_addr = BleAddr::from_bluer(addr, &self.adapter_name);
|
||||||
|
let psm =
|
||||||
|
device.service_data().await.ok().flatten().and_then(|sd| {
|
||||||
|
sd.get(&PSM_SERVICE_DATA_UUID)
|
||||||
|
.and_then(|data| psm::decode_psm(data))
|
||||||
|
});
|
||||||
|
let rssi = device.rssi().await.ok().flatten();
|
||||||
|
debug!(addr = %ble_addr, ?psm, ?rssi, "BLE scanner: FIPS peer found");
|
||||||
|
return Some(ScanAdvert {
|
||||||
|
addr: ble_addr,
|
||||||
|
psm,
|
||||||
|
rssi,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(_) => {
|
||||||
|
trace!(addr = %addr, "BLE scanner: device without FIPS UUID");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
trace!(addr = %addr, error = %e, "BLE scanner: failed to read UUIDs");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(_) => continue,
|
||||||
|
None => return None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// BluerIo
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Production BLE I/O implementation via BlueZ D-Bus (bluer crate).
|
||||||
|
pub struct BluerIo {
|
||||||
|
#[allow(dead_code)] // Session must be kept alive for the adapter.
|
||||||
|
session: bluer::Session,
|
||||||
|
adapter: bluer::Adapter,
|
||||||
|
adapter_name: String,
|
||||||
|
adv_handle: Mutex<Option<bluer::adv::AdvertisementHandle>>,
|
||||||
|
mtu: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BluerIo {
|
||||||
|
/// Create a new BluerIo for the given adapter.
|
||||||
|
///
|
||||||
|
/// Connects to BlueZ via D-Bus and powers on the adapter.
|
||||||
|
pub async fn new(adapter_name: &str, mtu: u16) -> Result<Self, TransportError> {
|
||||||
|
let session = bluer::Session::new()
|
||||||
|
.await
|
||||||
|
.map_err(|e| map_err("Session::new", e))?;
|
||||||
|
|
||||||
|
let adapter = if adapter_name == "default" {
|
||||||
|
session
|
||||||
|
.default_adapter()
|
||||||
|
.await
|
||||||
|
.map_err(|e| map_err("default_adapter", e))?
|
||||||
|
} else {
|
||||||
|
session
|
||||||
|
.adapter(adapter_name)
|
||||||
|
.map_err(|e| map_err("adapter", e))?
|
||||||
|
};
|
||||||
|
|
||||||
|
adapter
|
||||||
|
.set_powered(true)
|
||||||
|
.await
|
||||||
|
.map_err(|e| map_err("set_powered", e))?;
|
||||||
|
|
||||||
|
let name = adapter.name().to_string();
|
||||||
|
debug!(adapter = %name, "BluerIo initialized");
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
session,
|
||||||
|
adapter,
|
||||||
|
adapter_name: name,
|
||||||
|
adv_handle: Mutex::new(None),
|
||||||
|
mtu,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BleIo for BluerIo {
|
||||||
|
type Stream = BluerStream;
|
||||||
|
type Acceptor = BluerAcceptor;
|
||||||
|
type Scanner = BluerScanner;
|
||||||
|
|
||||||
|
/// Binds the requested PSM and reports it back unchanged.
|
||||||
|
///
|
||||||
|
/// BlueZ lets an application choose the PSM it binds, so the bound
|
||||||
|
/// PSM is always the requested one. Backends whose platform assigns
|
||||||
|
/// the PSM report something else; that is the reason for the return
|
||||||
|
/// value, not anything BlueZ does.
|
||||||
|
async fn listen(&self, psm: u16) -> Result<(Self::Acceptor, u16), TransportError> {
|
||||||
|
let local_addr = self
|
||||||
|
.adapter
|
||||||
|
.address()
|
||||||
|
.await
|
||||||
|
.map_err(|e| map_err("address", e))?;
|
||||||
|
|
||||||
|
let sa = SocketAddr::new(local_addr, AddressType::LePublic, psm);
|
||||||
|
let listener = SeqPacketListener::bind(sa)
|
||||||
|
.await
|
||||||
|
.map_err(|e| map_io_err("bind", e))?;
|
||||||
|
|
||||||
|
// Request high MTU for accepted connections
|
||||||
|
listener
|
||||||
|
.as_ref()
|
||||||
|
.set_recv_mtu(self.mtu)
|
||||||
|
.map_err(|e| map_io_err("set_recv_mtu", e))?;
|
||||||
|
|
||||||
|
// Prevent sniff mode to reduce latency during data transfer
|
||||||
|
if let Err(e) = listener.as_ref().set_power_forced_active(true) {
|
||||||
|
debug!(error = %e, "BLE listener: set_power_forced_active not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!(psm, mtu = self.mtu, "BLE listener bound");
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
BluerAcceptor {
|
||||||
|
listener,
|
||||||
|
adapter_name: self.adapter_name.clone(),
|
||||||
|
},
|
||||||
|
psm,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect(&self, addr: &BleAddr, psm: u16) -> Result<Self::Stream, TransportError> {
|
||||||
|
let target_sa = addr.to_socket_addr(psm);
|
||||||
|
|
||||||
|
let socket =
|
||||||
|
Socket::<SeqPacket>::new_seq_packet().map_err(|e| map_io_err("new_seq_packet", e))?;
|
||||||
|
socket
|
||||||
|
.bind(SocketAddr::any_le())
|
||||||
|
.map_err(|e| map_io_err("bind", e))?;
|
||||||
|
socket
|
||||||
|
.set_recv_mtu(self.mtu)
|
||||||
|
.map_err(|e| map_io_err("set_recv_mtu", e))?;
|
||||||
|
|
||||||
|
// Prevent sniff mode to reduce latency during data transfer
|
||||||
|
if let Err(e) = socket.set_power_forced_active(true) {
|
||||||
|
debug!(error = %e, "BLE connect: set_power_forced_active not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
let conn = socket
|
||||||
|
.connect(target_sa)
|
||||||
|
.await
|
||||||
|
.map_err(|e| map_io_err("connect", e))?;
|
||||||
|
|
||||||
|
let remote = addr.clone();
|
||||||
|
BluerStream::new(conn, remote)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Advertises the FIPS service UUID and the listener PSM.
|
||||||
|
///
|
||||||
|
/// The PSM rides the Service Data AD structure specified in
|
||||||
|
/// `super::super::psm`. Emitting it costs the `local_name`: flags
|
||||||
|
/// (3) + 128-bit UUID list (18) + service data (6) fill 27 of the
|
||||||
|
/// 31-byte legacy PDU, and a name no longer fits. Peers that read
|
||||||
|
/// the service data dial the advertised PSM; legacy peers keep
|
||||||
|
/// dialling their configured one, which BlueZ listeners still bind.
|
||||||
|
///
|
||||||
|
/// `super::super::psm` requires the PSM to ride the primary
|
||||||
|
/// advertisement, never the scan response, so a passive scanner
|
||||||
|
/// still sees it. Nothing here enforces that: BlueZ takes a set of
|
||||||
|
/// AD structures and chooses their placement itself. What keeps the
|
||||||
|
/// requirement holding is the arithmetic above — 27 of 31 bytes
|
||||||
|
/// used, so BlueZ has no reason to spill into the scan response —
|
||||||
|
/// and dropping the name is what makes it hold.
|
||||||
|
async fn start_advertising(&self, psm: u16) -> Result<(), TransportError> {
|
||||||
|
let adv = Advertisement {
|
||||||
|
advertisement_type: bluer::adv::Type::Peripheral,
|
||||||
|
service_uuids: {
|
||||||
|
let mut s = BTreeSet::new();
|
||||||
|
s.insert(FIPS_SERVICE_UUID);
|
||||||
|
s
|
||||||
|
},
|
||||||
|
service_data: {
|
||||||
|
let mut m = BTreeMap::new();
|
||||||
|
m.insert(PSM_SERVICE_DATA_UUID, psm::encode_psm(psm).to_vec());
|
||||||
|
m
|
||||||
|
},
|
||||||
|
min_interval: Some(std::time::Duration::from_millis(400)),
|
||||||
|
max_interval: Some(std::time::Duration::from_millis(600)),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let handle = self
|
||||||
|
.adapter
|
||||||
|
.advertise(adv)
|
||||||
|
.await
|
||||||
|
.map_err(|e| map_err("advertise", e))?;
|
||||||
|
|
||||||
|
*self.adv_handle.lock().await = Some(handle);
|
||||||
|
debug!(psm, "BLE advertising started");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn stop_advertising(&self) -> Result<(), TransportError> {
|
||||||
|
let _ = self.adv_handle.lock().await.take();
|
||||||
|
debug!("BLE advertising stopped");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start_scanning(&self) -> Result<Self::Scanner, TransportError> {
|
||||||
|
// Clear cached devices so BlueZ fires DeviceAdded for every
|
||||||
|
// advertisement. Without this, already-known devices only
|
||||||
|
// produce PropertyChanged events (which bluer doesn't expose
|
||||||
|
// at the device level), causing the scanner to miss peers
|
||||||
|
// after a daemon restart.
|
||||||
|
if let Ok(cached) = self.adapter.device_addresses().await {
|
||||||
|
let count = cached.len();
|
||||||
|
for addr in cached {
|
||||||
|
let _ = self.adapter.remove_device(addr).await;
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
debug!(count, "BLE scanner: cleared cached devices");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set discovery filter for LE transport with FIPS UUID
|
||||||
|
let filter = DiscoveryFilter {
|
||||||
|
transport: DiscoveryTransport::Le,
|
||||||
|
uuids: {
|
||||||
|
let mut s = HashSet::new();
|
||||||
|
s.insert(FIPS_SERVICE_UUID);
|
||||||
|
s
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
self.adapter
|
||||||
|
.set_discovery_filter(filter)
|
||||||
|
.await
|
||||||
|
.map_err(|e| map_err("set_discovery_filter", e))?;
|
||||||
|
|
||||||
|
let events = self
|
||||||
|
.adapter
|
||||||
|
.discover_devices()
|
||||||
|
.await
|
||||||
|
.map_err(|e| map_err("discover_devices", e))?;
|
||||||
|
|
||||||
|
debug!("BLE scanning started");
|
||||||
|
|
||||||
|
Ok(BluerScanner {
|
||||||
|
events: Box::pin(events),
|
||||||
|
adapter: self.adapter.clone(),
|
||||||
|
adapter_name: self.adapter_name.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn local_addr(&self) -> Result<BleAddr, TransportError> {
|
||||||
|
// Use futures::executor::block_on since this is a sync method
|
||||||
|
// but needs an async call. The adapter address is cached so
|
||||||
|
// the D-Bus call is fast.
|
||||||
|
let addr = futures::executor::block_on(self.adapter.address())
|
||||||
|
.map_err(|e| map_err("address", e))?;
|
||||||
|
Ok(BleAddr::from_bluer(addr, &self.adapter_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn adapter_name(&self) -> &str {
|
||||||
|
&self.adapter_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile-time assertion that BluerIo satisfies Send + Sync.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn _assert_bluer_io_send_sync() {
|
||||||
|
fn require<T: Send + Sync>() {}
|
||||||
|
require::<BluerIo>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// A wrong shift in the base-UUID expansion would yield a plausible
|
||||||
|
/// UUID that simply never matches any peer — silent discovery
|
||||||
|
/// failure, not a build error. Companion to psm.rs's
|
||||||
|
/// `test_key_is_the_leading_16_bits_of_the_fips_uuid`.
|
||||||
|
#[test]
|
||||||
|
fn psm_service_data_uuid_expands_the_key_over_the_base_uuid() {
|
||||||
|
assert_eq!(
|
||||||
|
PSM_SERVICE_DATA_UUID,
|
||||||
|
"00009C90-0000-1000-8000-00805F9B34FB"
|
||||||
|
.parse::<bluer::Uuid>()
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+201
-15
@@ -11,7 +11,7 @@
|
|||||||
//! may return a fragment of a packet or several packets coalesced from one
|
//! may return a fragment of a packet or several packets coalesced from one
|
||||||
//! read. The receive path therefore recovers boundaries from the FMP length
|
//! read. The receive path therefore recovers boundaries from the FMP length
|
||||||
//! prefix via [`stream_read::BleStreamRead`] and
|
//! prefix via [`stream_read::BleStreamRead`] and
|
||||||
//! [`crate::transport::framing::read_fmp_packet`], which is a transparent
|
//! `crate::transport::framing::read_fmp_packet`, which is a transparent
|
||||||
//! pass-through on a boundary-preserving backend.
|
//! pass-through on a boundary-preserving backend.
|
||||||
//!
|
//!
|
||||||
//! ## Architecture
|
//! ## Architecture
|
||||||
@@ -29,8 +29,11 @@
|
|||||||
|
|
||||||
pub mod addr;
|
pub mod addr;
|
||||||
pub mod io;
|
pub mod io;
|
||||||
|
#[cfg(bluer_available)]
|
||||||
|
pub mod io_linux;
|
||||||
pub mod neighbor;
|
pub mod neighbor;
|
||||||
pub mod pool;
|
pub mod pool;
|
||||||
|
pub mod psm;
|
||||||
pub mod stats;
|
pub mod stats;
|
||||||
pub mod stream_read;
|
pub mod stream_read;
|
||||||
|
|
||||||
@@ -59,6 +62,12 @@ use tracing::{debug, info, trace, warn};
|
|||||||
/// Default FIPS L2CAP PSM (Protocol Service Multiplexer).
|
/// Default FIPS L2CAP PSM (Protocol Service Multiplexer).
|
||||||
///
|
///
|
||||||
/// 0x0085 (133) is in the dynamic range (0x0080-0x00FF).
|
/// 0x0085 (133) is in the dynamic range (0x0080-0x00FF).
|
||||||
|
///
|
||||||
|
/// This is a request and a fallback, not a guarantee. A backend whose
|
||||||
|
/// platform assigns the PSM reports back what it actually bound (see
|
||||||
|
/// [`io::BleIo::listen`]), and a peer that advertises its own PSM (see
|
||||||
|
/// [`psm`]) is dialled there instead. The configured value is what a peer is
|
||||||
|
/// dialled at when it advertises nothing.
|
||||||
pub const DEFAULT_PSM: u16 = 0x0085;
|
pub const DEFAULT_PSM: u16 = 0x0085;
|
||||||
|
|
||||||
/// Concrete BLE transport type for use in TransportHandle.
|
/// Concrete BLE transport type for use in TransportHandle.
|
||||||
@@ -66,7 +75,7 @@ pub const DEFAULT_PSM: u16 = 0x0085;
|
|||||||
/// Production builds on glibc-linux use `BluerIo` (real BlueZ stack).
|
/// Production builds on glibc-linux use `BluerIo` (real BlueZ stack).
|
||||||
/// Test builds, musl-linux, and non-Linux platforms use `MockBleIo`.
|
/// Test builds, musl-linux, and non-Linux platforms use `MockBleIo`.
|
||||||
#[cfg(all(bluer_available, not(test)))]
|
#[cfg(all(bluer_available, not(test)))]
|
||||||
pub type DefaultBleTransport = BleTransport<io::BluerIo>;
|
pub type DefaultBleTransport = BleTransport<io_linux::BluerIo>;
|
||||||
|
|
||||||
#[cfg(any(not(bluer_available), test))]
|
#[cfg(any(not(bluer_available), test))]
|
||||||
pub type DefaultBleTransport = BleTransport<io::MockBleIo>;
|
pub type DefaultBleTransport = BleTransport<io::MockBleIo>;
|
||||||
@@ -177,9 +186,14 @@ impl<I: BleIo> BleTransport<I> {
|
|||||||
}
|
}
|
||||||
self.state = TransportState::Starting;
|
self.state = TransportState::Starting;
|
||||||
|
|
||||||
let psm = self.config.psm();
|
let configured_psm = self.config.psm();
|
||||||
let adapter = self.io.adapter_name().to_string();
|
let adapter = self.io.adapter_name().to_string();
|
||||||
|
|
||||||
|
// The PSM peers should dial us on. Only the listener knows it: a
|
||||||
|
// backend whose platform assigns PSMs reports back something other
|
||||||
|
// than what was requested, and that is what has to be advertised.
|
||||||
|
let mut listener_psm = configured_psm;
|
||||||
|
|
||||||
// Pre-compute local NodeAddr for cross-probe tie-breaking
|
// Pre-compute local NodeAddr for cross-probe tie-breaking
|
||||||
let local_node_addr = self.local_pubkey.and_then(|pk| {
|
let local_node_addr = self.local_pubkey.and_then(|pk| {
|
||||||
XOnlyPublicKey::from_slice(&pk)
|
XOnlyPublicKey::from_slice(&pk)
|
||||||
@@ -189,8 +203,9 @@ impl<I: BleIo> BleTransport<I> {
|
|||||||
|
|
||||||
// Start L2CAP listener for inbound connections
|
// Start L2CAP listener for inbound connections
|
||||||
if self.config.accept_connections() {
|
if self.config.accept_connections() {
|
||||||
match self.io.listen(psm).await {
|
match self.io.listen(configured_psm).await {
|
||||||
Ok(acceptor) => {
|
Ok((acceptor, bound_psm)) => {
|
||||||
|
listener_psm = bound_psm;
|
||||||
let pool = Arc::clone(&self.pool);
|
let pool = Arc::clone(&self.pool);
|
||||||
let packet_tx = self.packet_tx.clone();
|
let packet_tx = self.packet_tx.clone();
|
||||||
let transport_id = self.transport_id;
|
let transport_id = self.transport_id;
|
||||||
@@ -208,7 +223,12 @@ impl<I: BleIo> BleTransport<I> {
|
|||||||
Arc::clone(&self.neighbor_buffer),
|
Arc::clone(&self.neighbor_buffer),
|
||||||
local_node_addr,
|
local_node_addr,
|
||||||
)));
|
)));
|
||||||
debug!(adapter = %adapter, psm = psm, "BLE accept loop started");
|
debug!(
|
||||||
|
adapter = %adapter,
|
||||||
|
psm = listener_psm,
|
||||||
|
requested_psm = configured_psm,
|
||||||
|
"BLE accept loop started"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!(adapter = %adapter, error = %e, "failed to start BLE listener");
|
warn!(adapter = %adapter, error = %e, "failed to start BLE listener");
|
||||||
@@ -220,11 +240,15 @@ impl<I: BleIo> BleTransport<I> {
|
|||||||
|
|
||||||
// Start continuous advertising
|
// Start continuous advertising
|
||||||
if self.config.advertise() {
|
if self.config.advertise() {
|
||||||
if let Err(e) = self.io.start_advertising().await {
|
if let Err(e) = self.io.start_advertising(listener_psm).await {
|
||||||
warn!(adapter = %adapter, error = %e, "failed to start BLE advertising");
|
warn!(adapter = %adapter, error = %e, "failed to start BLE advertising");
|
||||||
} else {
|
} else {
|
||||||
self.stats.record_advertisement();
|
self.stats.record_advertisement();
|
||||||
debug!(adapter = %adapter, "BLE advertising started (continuous)");
|
debug!(
|
||||||
|
adapter = %adapter,
|
||||||
|
psm = listener_psm,
|
||||||
|
"BLE advertising started (continuous)"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +279,7 @@ impl<I: BleIo> BleTransport<I> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.state = TransportState::Up;
|
self.state = TransportState::Up;
|
||||||
info!(adapter = %adapter, psm = psm, "BLE transport started");
|
info!(adapter = %adapter, psm = listener_psm, "BLE transport started");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -999,7 +1023,7 @@ async fn scan_probe_loop<I: io::BleIo>(
|
|||||||
buffer: Arc<NeighborBuffer>,
|
buffer: Arc<NeighborBuffer>,
|
||||||
stats: Arc<BleStats>,
|
stats: Arc<BleStats>,
|
||||||
local_pubkey: Option<[u8; 32]>,
|
local_pubkey: Option<[u8; 32]>,
|
||||||
psm: u16,
|
configured_psm: u16,
|
||||||
connect_timeout_ms: u64,
|
connect_timeout_ms: u64,
|
||||||
cooldown_secs: u64,
|
cooldown_secs: u64,
|
||||||
local_node_addr: Option<NodeAddr>,
|
local_node_addr: Option<NodeAddr>,
|
||||||
@@ -1018,6 +1042,13 @@ async fn scan_probe_loop<I: io::BleIo>(
|
|||||||
// one per rotation, so entries are dropped once their node is no longer in
|
// one per rotation, so entries are dropped once their node is no longer in
|
||||||
// the pool — a peer that genuinely goes away is probed again normally.
|
// the pool — a peer that genuinely goes away is probed again normally.
|
||||||
let mut known_node_of: HashMap<BleAddr, NodeAddr> = HashMap::new();
|
let mut known_node_of: HashMap<BleAddr, NodeAddr> = HashMap::new();
|
||||||
|
// L2CAP listener PSMs read out of peers' advertisements. A peer whose
|
||||||
|
// platform assigns its listener PSM cannot be dialled at a configured
|
||||||
|
// constant, so it publishes the number it actually bound and we dial
|
||||||
|
// that. A peer that advertises nothing is dialled at `configured_psm`,
|
||||||
|
// which is every peer that predates this and every backend that does not
|
||||||
|
// advertise service data.
|
||||||
|
let mut learned_psm: HashMap<BleAddr, u16> = HashMap::new();
|
||||||
let cooldown = std::time::Duration::from_secs(cooldown_secs);
|
let cooldown = std::time::Duration::from_secs(cooldown_secs);
|
||||||
let retry_interval = tokio::time::interval(std::time::Duration::from_secs(cooldown_secs));
|
let retry_interval = tokio::time::interval(std::time::Duration::from_secs(cooldown_secs));
|
||||||
tokio::pin!(retry_interval);
|
tokio::pin!(retry_interval);
|
||||||
@@ -1028,7 +1059,13 @@ async fn scan_probe_loop<I: io::BleIo>(
|
|||||||
let addr = tokio::select! {
|
let addr = tokio::select! {
|
||||||
result = scanner.next() => {
|
result = scanner.next() => {
|
||||||
match result {
|
match result {
|
||||||
Some(a) => a,
|
Some(advert) => {
|
||||||
|
if let Some(psm) = advert.psm {
|
||||||
|
trace!(addr = %advert.addr, psm, "BLE scan: learned peer PSM");
|
||||||
|
learned_psm.insert(advert.addr.clone(), psm);
|
||||||
|
}
|
||||||
|
advert.addr
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
debug!("BLE scanner ended");
|
debug!("BLE scanner ended");
|
||||||
break;
|
break;
|
||||||
@@ -1102,21 +1139,27 @@ async fn scan_probe_loop<I: io::BleIo>(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// L2CAP connect
|
// L2CAP connect, at whatever PSM this peer advertised.
|
||||||
|
let dial_psm = learned_psm.get(&addr).copied().unwrap_or(configured_psm);
|
||||||
let stream = match tokio::time::timeout(
|
let stream = match tokio::time::timeout(
|
||||||
std::time::Duration::from_millis(connect_timeout_ms),
|
std::time::Duration::from_millis(connect_timeout_ms),
|
||||||
io.connect(&addr, psm),
|
io.connect(&addr, dial_psm),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(Ok(s)) => s,
|
Ok(Ok(s)) => s,
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
debug!(addr = %addr, error = %e, "BLE probe connect failed");
|
debug!(addr = %addr, psm = dial_psm, error = %e, "BLE probe connect failed");
|
||||||
|
// A learned PSM that does not answer is stale — forget it, so
|
||||||
|
// the next advert re-learns it and the fallback applies in the
|
||||||
|
// meantime. Costs one retry.
|
||||||
|
learned_psm.remove(&addr);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
debug!(addr = %addr, "BLE probe connect timeout");
|
debug!(addr = %addr, psm = dial_psm, "BLE probe connect timeout");
|
||||||
stats.record_connect_timeout();
|
stats.record_connect_timeout();
|
||||||
|
learned_psm.remove(&addr);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1735,6 +1778,149 @@ mod tests {
|
|||||||
transport.stop_async().await.unwrap();
|
transport.stop_async().await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Per-peer listener PSM
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Every `(address, psm)` the transport tried to dial.
|
||||||
|
type DialLog = Arc<std::sync::Mutex<Vec<(BleAddr, u16)>>>;
|
||||||
|
|
||||||
|
/// A scanning transport whose dials all fail, recording the PSM each was
|
||||||
|
/// attempted at.
|
||||||
|
fn psm_probe_transport(
|
||||||
|
dials: DialLog,
|
||||||
|
) -> (
|
||||||
|
BleTransport<MockBleIo>,
|
||||||
|
tokio::sync::mpsc::Receiver<ReceivedPacket>,
|
||||||
|
) {
|
||||||
|
let io = MockBleIo::new("hci0", test_addr(1));
|
||||||
|
io.set_connect_handler(move |addr, psm| {
|
||||||
|
dials.lock().unwrap().push((addr.clone(), psm));
|
||||||
|
Err(TransportError::ConnectionRefused)
|
||||||
|
});
|
||||||
|
let config = BleConfig {
|
||||||
|
adapter: Some("hci0".to_string()),
|
||||||
|
scan: Some(true),
|
||||||
|
advertise: Some(false),
|
||||||
|
accept_connections: Some(false),
|
||||||
|
probe_cooldown_secs: Some(1),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let (tx, rx) = tokio::sync::mpsc::channel(64);
|
||||||
|
let mut transport = BleTransport::new(TransportId::new(1), None, config, io, tx);
|
||||||
|
transport.set_local_pubkey(test_pubkey(1));
|
||||||
|
(transport, rx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A peer that advertises its listener PSM is dialled there, not at the
|
||||||
|
/// configured one — the whole point of learning it.
|
||||||
|
#[tokio::test(start_paused = true)]
|
||||||
|
async fn test_advertised_psm_is_dialled() {
|
||||||
|
let dials: DialLog = Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||||
|
let (mut transport, _rx) = psm_probe_transport(Arc::clone(&dials));
|
||||||
|
transport.start_async().await.unwrap();
|
||||||
|
|
||||||
|
transport
|
||||||
|
.io
|
||||||
|
.inject_scan_advert(io::ScanAdvert::with_psm(test_addr(2), 0x00C1))
|
||||||
|
.await;
|
||||||
|
settle().await;
|
||||||
|
|
||||||
|
assert_eq!(dials.lock().unwrap().as_slice(), &[(test_addr(2), 0x00C1)]);
|
||||||
|
transport.stop_async().await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A legacy UUID-only advertiser carries no PSM, so the configured one is
|
||||||
|
/// used. This is the path every existing peer takes and it must not
|
||||||
|
/// regress.
|
||||||
|
#[tokio::test(start_paused = true)]
|
||||||
|
async fn test_advert_without_a_psm_falls_back_to_the_configured_one() {
|
||||||
|
let dials: DialLog = Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||||
|
let (mut transport, _rx) = psm_probe_transport(Arc::clone(&dials));
|
||||||
|
transport.start_async().await.unwrap();
|
||||||
|
|
||||||
|
transport.io.inject_scan_result(test_addr(2)).await;
|
||||||
|
settle().await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
dials.lock().unwrap().as_slice(),
|
||||||
|
&[(test_addr(2), DEFAULT_PSM)]
|
||||||
|
);
|
||||||
|
transport.stop_async().await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A learned PSM that does not answer is forgotten, so a stale value
|
||||||
|
/// costs one retry rather than making the peer permanently unreachable.
|
||||||
|
#[tokio::test(start_paused = true)]
|
||||||
|
async fn test_a_failed_dial_forgets_the_learned_psm() {
|
||||||
|
let dials: DialLog = Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||||
|
let (mut transport, _rx) = psm_probe_transport(Arc::clone(&dials));
|
||||||
|
transport.start_async().await.unwrap();
|
||||||
|
|
||||||
|
transport
|
||||||
|
.io
|
||||||
|
.inject_scan_advert(io::ScanAdvert::with_psm(test_addr(2), 0x00C1))
|
||||||
|
.await;
|
||||||
|
settle().await;
|
||||||
|
assert_eq!(dials.lock().unwrap().len(), 1);
|
||||||
|
|
||||||
|
// The retry after the cooldown must not repeat the PSM that failed.
|
||||||
|
tokio::time::advance(std::time::Duration::from_secs(3)).await;
|
||||||
|
settle().await;
|
||||||
|
|
||||||
|
let log = dials.lock().unwrap().clone();
|
||||||
|
assert!(log.len() >= 2, "the address is retried after the cooldown");
|
||||||
|
assert_eq!(log[0], (test_addr(2), 0x00C1));
|
||||||
|
assert!(
|
||||||
|
log[1..].iter().all(|(_, psm)| *psm == DEFAULT_PSM),
|
||||||
|
"retries fall back to the configured PSM: {log:?}"
|
||||||
|
);
|
||||||
|
transport.stop_async().await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The advertisement carries the PSM the listener actually bound, not the
|
||||||
|
/// one that was requested. This is the whole OS-assigned-PSM case, with
|
||||||
|
/// no platform in the assertion.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_the_advertised_psm_is_the_one_actually_bound() {
|
||||||
|
let io = MockBleIo::new("hci0", test_addr(1));
|
||||||
|
io.set_bound_psm(0x00C1);
|
||||||
|
let config = BleConfig {
|
||||||
|
adapter: Some("hci0".to_string()),
|
||||||
|
scan: Some(false),
|
||||||
|
advertise: Some(true),
|
||||||
|
accept_connections: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let (tx, _rx) = tokio::sync::mpsc::channel(64);
|
||||||
|
let mut transport = BleTransport::new(TransportId::new(1), None, config, io, tx);
|
||||||
|
transport.start_async().await.unwrap();
|
||||||
|
|
||||||
|
assert_ne!(DEFAULT_PSM, 0x00C1, "test setup: the bound PSM differs");
|
||||||
|
assert_eq!(transport.io.advertised_psm(), Some(0x00C1));
|
||||||
|
transport.stop_async().await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A backend that binds what it was asked for advertises that — the BlueZ
|
||||||
|
/// path, unchanged.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_a_backend_that_honours_the_request_advertises_it() {
|
||||||
|
let io = MockBleIo::new("hci0", test_addr(1));
|
||||||
|
let config = BleConfig {
|
||||||
|
adapter: Some("hci0".to_string()),
|
||||||
|
scan: Some(false),
|
||||||
|
advertise: Some(true),
|
||||||
|
accept_connections: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let (tx, _rx) = tokio::sync::mpsc::channel(64);
|
||||||
|
let mut transport = BleTransport::new(TransportId::new(1), None, config, io, tx);
|
||||||
|
transport.start_async().await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(transport.io.advertised_psm(), Some(DEFAULT_PSM));
|
||||||
|
transport.stop_async().await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
/// A peer that opens with something other than the exchange prefix is
|
/// A peer that opens with something other than the exchange prefix is
|
||||||
/// rejected before the framer ever sees the bytes.
|
/// rejected before the framer ever sees the bytes.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
//! BLE neighbor detection via advertising and scanning.
|
//! BLE neighbor detection via advertising and scanning.
|
||||||
//!
|
//!
|
||||||
//! BLE advertisements carry a 128-bit FIPS service UUID for identification.
|
//! BLE advertisements carry a 128-bit FIPS service UUID for identification,
|
||||||
//! Post-forklift, advertisements are UUID-only (no identity material);
|
//! and optionally the advertiser's L2CAP listener PSM (see `super::psm`).
|
||||||
//! identity is exchanged during the Noise handshake.
|
//! Post-forklift they carry no identity material; identity is exchanged
|
||||||
|
//! during the Noise handshake.
|
||||||
|
|
||||||
use crate::transport::{DiscoveredPeer, TransportId};
|
use crate::transport::{DiscoveredPeer, TransportId};
|
||||||
use secp256k1::XOnlyPublicKey;
|
use secp256k1::XOnlyPublicKey;
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
//! Advertising the L2CAP listener PSM.
|
||||||
|
//!
|
||||||
|
//! A dialer has to know which PSM a peer's L2CAP listener is bound to. On
|
||||||
|
//! BlueZ an application can *choose* that number, so both ends can agree on a
|
||||||
|
//! configured constant. BlueZ is the exception: Android's
|
||||||
|
//! `listenUsingInsecureL2capChannel` and macOS's
|
||||||
|
//! `CBPeripheralManager.publishL2CAPChannel` both return an **OS-assigned**
|
||||||
|
//! PSM the application cannot request. A dialer cannot guess it, and before a
|
||||||
|
//! connection exists there is no channel to be told it on other than the
|
||||||
|
//! advertisement itself.
|
||||||
|
//!
|
||||||
|
//! This module is the wire specification for putting it there. It is
|
||||||
|
//! deliberately state-free: learning and caching belong to the scan/probe
|
||||||
|
//! loop, which already owns per-address state.
|
||||||
|
//!
|
||||||
|
//! # Wire layout
|
||||||
|
//!
|
||||||
|
//! The PSM rides a **Service Data — 16-bit UUID** AD structure (AD type
|
||||||
|
//! `0x16`) keyed on [`PSM_SERVICE_DATA_UUID16`], carrying the PSM as two
|
||||||
|
//! bytes little-endian.
|
||||||
|
//!
|
||||||
|
//! ## Why a 16-bit key, and not the FIPS service UUID
|
||||||
|
//!
|
||||||
|
//! A legacy advertising PDU carries 31 bytes of AD payload. Keying the
|
||||||
|
//! service data on the full 128-bit FIPS service UUID does not fit:
|
||||||
|
//!
|
||||||
|
//! | AD structure | bytes |
|
||||||
|
//! |-----------------------------------------------|-------|
|
||||||
|
//! | Flags | 3 |
|
||||||
|
//! | Complete list of 128-bit service UUIDs | 18 |
|
||||||
|
//! | Service Data — **128-bit** UUID + 2-byte PSM | 20 |
|
||||||
|
//! | **total** | **41** — over by 10 |
|
||||||
|
//!
|
||||||
|
//! Keying it on the 16-bit UUID [`PSM_SERVICE_DATA_UUID16`] does:
|
||||||
|
//!
|
||||||
|
//! | AD structure | bytes |
|
||||||
|
//! |-----------------------------------------------|-------|
|
||||||
|
//! | Flags | 3 |
|
||||||
|
//! | Complete list of 128-bit service UUIDs | 18 |
|
||||||
|
//! | Service Data — **16-bit** UUID + 2-byte PSM | 6 |
|
||||||
|
//! | **total** | **27** — fits |
|
||||||
|
//!
|
||||||
|
//! `0x9C90` is the leading 16 bits of the FIPS service UUID, expanded through
|
||||||
|
//! the Bluetooth base UUID (`00009C90-0000-1000-8000-00805F9B34FB`). The
|
||||||
|
//! budget is asserted at compile time below, so a change that reverts to a
|
||||||
|
//! 128-bit key fails the build rather than the radio. It also means an
|
||||||
|
//! advertiser using this layout has no room left for a local name.
|
||||||
|
//!
|
||||||
|
//! ## Why the primary advertisement, not the scan response
|
||||||
|
//!
|
||||||
|
//! A scan response only arrives after a successful active-scan
|
||||||
|
//! request/response round-trip, and that round-trip drops asymmetrically
|
||||||
|
//! across chipsets. Peers that never answer a scan request would become
|
||||||
|
//! undiscoverable rather than merely slower. The primary advertisement is
|
||||||
|
//! received passively on every advertising interval, so the PSM must ride it.
|
||||||
|
//!
|
||||||
|
//! ## Compatibility
|
||||||
|
//!
|
||||||
|
//! A reader ignores trailing bytes, so the value can be extended without
|
||||||
|
//! breaking older peers, and an advert with no service data at all decodes to
|
||||||
|
//! `None` — which is what every legacy UUID-only advertiser produces, and
|
||||||
|
//! what makes them keep working against the configured PSM.
|
||||||
|
|
||||||
|
/// Service-data key for the advertised L2CAP PSM.
|
||||||
|
///
|
||||||
|
/// The leading 16 bits of the FIPS service UUID, i.e. the Bluetooth
|
||||||
|
/// base-range UUID `00009C90-0000-1000-8000-00805F9B34FB`. Backends that
|
||||||
|
/// speak in whole UUIDs must expand it through the base UUID; backends that
|
||||||
|
/// speak in AD structures emit it as AD type `0x16`.
|
||||||
|
pub const PSM_SERVICE_DATA_UUID16: u16 = 0x9C90;
|
||||||
|
|
||||||
|
/// AD payload budget of a legacy advertising PDU, in bytes.
|
||||||
|
const LEGACY_ADV_PAYLOAD_BYTES: usize = 31;
|
||||||
|
|
||||||
|
/// Flags AD structure: length + type + one byte of flags.
|
||||||
|
const FLAGS_AD_BYTES: usize = 3;
|
||||||
|
|
||||||
|
/// Complete list of 128-bit service UUIDs: length + type + one UUID.
|
||||||
|
const UUID128_LIST_AD_BYTES: usize = 2 + 16;
|
||||||
|
|
||||||
|
/// Service data keyed on a 16-bit UUID: length + type + key + PSM.
|
||||||
|
const PSM_SERVICE_DATA_AD_BYTES: usize = 2 + 2 + PSM_ENCODED_LEN;
|
||||||
|
|
||||||
|
/// Encoded width of the PSM value itself.
|
||||||
|
const PSM_ENCODED_LEN: usize = 2;
|
||||||
|
|
||||||
|
/// The layout above must fit a legacy advertising PDU. If this fails, the
|
||||||
|
/// advert would be silently truncated or rejected by the controller.
|
||||||
|
const _: () = assert!(
|
||||||
|
FLAGS_AD_BYTES + UUID128_LIST_AD_BYTES + PSM_SERVICE_DATA_AD_BYTES <= LEGACY_ADV_PAYLOAD_BYTES,
|
||||||
|
"PSM advert layout exceeds the 31-byte legacy advertising PDU"
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Encode a PSM as advertised service data: two bytes, little-endian.
|
||||||
|
pub fn encode_psm(psm: u16) -> [u8; PSM_ENCODED_LEN] {
|
||||||
|
psm.to_le_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode a PSM from advertised service data.
|
||||||
|
///
|
||||||
|
/// Returns `None` for absent or truncated data — a legacy UUID-only
|
||||||
|
/// advertiser, which the caller answers by dialling the configured PSM.
|
||||||
|
/// Trailing bytes are ignored so the value can be extended later without
|
||||||
|
/// breaking readers built against this version.
|
||||||
|
pub fn decode_psm(data: &[u8]) -> Option<u16> {
|
||||||
|
if data.len() < PSM_ENCODED_LEN {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(u16::from_le_bytes([data[0], data[1]]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Tests
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_encode_is_little_endian() {
|
||||||
|
assert_eq!(encode_psm(0x0085), [0x85, 0x00]);
|
||||||
|
assert_eq!(encode_psm(0x1234), [0x34, 0x12]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_round_trip() {
|
||||||
|
for psm in [0u16, 1, 0x0085, 0x00FF, 0x1234, u16::MAX] {
|
||||||
|
assert_eq!(decode_psm(&encode_psm(psm)), Some(psm), "psm {psm:#06x}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_absent_service_data_decodes_to_none() {
|
||||||
|
// A legacy UUID-only advertiser carries no service data at all.
|
||||||
|
assert_eq!(decode_psm(&[]), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_truncated_service_data_decodes_to_none() {
|
||||||
|
assert_eq!(decode_psm(&[0x85]), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_trailing_bytes_are_ignored() {
|
||||||
|
// Forward compatibility: a future advertiser may append fields.
|
||||||
|
assert_eq!(decode_psm(&[0x85, 0x00, 0xFF, 0xFF]), Some(0x0085));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The byte budget is a build-time assertion, not a comment. This test
|
||||||
|
/// records the arithmetic it encodes so the numbers stay legible.
|
||||||
|
#[test]
|
||||||
|
fn test_advert_fits_the_legacy_pdu() {
|
||||||
|
assert_eq!(FLAGS_AD_BYTES, 3);
|
||||||
|
assert_eq!(UUID128_LIST_AD_BYTES, 18);
|
||||||
|
assert_eq!(PSM_SERVICE_DATA_AD_BYTES, 6);
|
||||||
|
assert_eq!(
|
||||||
|
FLAGS_AD_BYTES + UUID128_LIST_AD_BYTES + PSM_SERVICE_DATA_AD_BYTES,
|
||||||
|
27
|
||||||
|
);
|
||||||
|
assert_eq!(LEGACY_ADV_PAYLOAD_BYTES, 31);
|
||||||
|
// A 128-bit service-data key would need 20 bytes, not 6 — the layout
|
||||||
|
// this module exists to reject.
|
||||||
|
assert_eq!(FLAGS_AD_BYTES + UUID128_LIST_AD_BYTES + 20, 41);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_key_is_the_leading_16_bits_of_the_fips_uuid() {
|
||||||
|
// FIPS service UUID: 9c90b790-2cc5-42c0-9f87-c9cc40648f4c
|
||||||
|
const FIPS_SERVICE_UUID_U128: u128 = 0x9c90_b790_2cc5_42c0_9f87_c9cc_4064_8f4c;
|
||||||
|
assert_eq!(
|
||||||
|
PSM_SERVICE_DATA_UUID16,
|
||||||
|
(FIPS_SERVICE_UUID_U128 >> 112) as u16
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
//! stream-oriented backend may return a fragment of a packet, or several
|
//! stream-oriented backend may return a fragment of a packet, or several
|
||||||
//! packets coalesced, from a single read. This adapter turns the
|
//! packets coalesced, from a single read. This adapter turns the
|
||||||
//! datagram-shaped [`BleStream`] into the [`AsyncRead`] that
|
//! datagram-shaped [`BleStream`] into the [`AsyncRead`] that
|
||||||
//! [`crate::transport::framing::read_fmp_packet`] expects, buffering bytes
|
//! `crate::transport::framing::read_fmp_packet` expects, buffering bytes
|
||||||
//! left over from one read into the next so packet boundaries are recovered
|
//! left over from one read into the next so packet boundaries are recovered
|
||||||
//! from the FMP length prefix rather than trusted to the OS.
|
//! from the FMP length prefix rather than trusted to the OS.
|
||||||
//!
|
//!
|
||||||
|
|||||||
Reference in New Issue
Block a user