mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-12 01:27:32 +00:00
Add BLE L2CAP transport with scan-based auto-connect
BLE transport implementation using L2CAP Connection-Oriented Channels (SeqPacket mode) via the bluer crate, behind cfg(feature = "ble"). Core transport: - BleTransport<I> generic over BleIo trait (BluerIo prod, MockBleIo test) - Connection pool with priority eviction (static > discovered, max 7) - Connect-on-send via connect_inline() matching TCP behavior - Per-connection receive loops with pool cleanup on disconnect Discovery and probing: - Combined scan_probe_loop using select! over scanner events and a BinaryHeap delay queue with per-entry random jitter (0-5s) to prevent herd effects when multiple nodes see the same beacon simultaneously - Pre-handshake pubkey exchange ([0x00][pubkey:32]) for IK identity - Cross-probe tie-breaker: smaller NodeAddr's outbound wins (same convention as FMP/FSP rekey dual-initiation) - Probed peers reported to DiscoveryBuffer; pool fills through normal node-layer auto-connect -> send_async -> connect_inline path Beacon management: - Periodic advertising: 1s burst every 30s (configurable via beacon_interval_secs / beacon_duration_secs) - FIPS service UUID for scan filtering Configuration (all fields optional with defaults): - adapter, psm, mtu, max_connections, connect_timeout_ms - advertise, scan, auto_connect, accept_connections - beacon_interval_secs (30), beacon_duration_secs (1) Hardware validated with two BLE nodes: - 2048-byte MTU, ~60-160ms RTT, zero-config auto-connect - BLE spike tool at testing/ble/ for standalone adapter validation 42 unit tests + 4 node-level integration tests, all CI-compatible via MockBleIo (no hardware required). tokio test-util added for time-dependent scan/probe tests.
This commit is contained in:
+1
-1
@@ -34,7 +34,7 @@ pub use node::{
|
||||
TreeConfig,
|
||||
};
|
||||
pub use peer::{ConnectPolicy, PeerAddress, PeerConfig};
|
||||
pub use transport::{DirectoryServiceConfig, EthernetConfig, TcpConfig, TorConfig, TransportInstances, TransportsConfig, UdpConfig};
|
||||
pub use transport::{BleConfig, DirectoryServiceConfig, EthernetConfig, TcpConfig, TorConfig, TransportInstances, TransportsConfig, UdpConfig};
|
||||
|
||||
/// Default config filename.
|
||||
const CONFIG_FILENAME: &str = "fips.yaml";
|
||||
|
||||
+159
-1
@@ -498,6 +498,153 @@ impl TorConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// BLE Transport Configuration
|
||||
// ============================================================================
|
||||
|
||||
/// Default BLE L2CAP PSM (dynamic range).
|
||||
const DEFAULT_BLE_PSM: u16 = 0x0085;
|
||||
|
||||
/// Default BLE MTU for L2CAP CoC connections.
|
||||
const DEFAULT_BLE_MTU: u16 = 2048;
|
||||
|
||||
/// Default maximum concurrent BLE connections.
|
||||
const DEFAULT_BLE_MAX_CONNECTIONS: usize = 7;
|
||||
|
||||
/// Default BLE connect timeout in milliseconds.
|
||||
const DEFAULT_BLE_CONNECT_TIMEOUT_MS: u64 = 10_000;
|
||||
|
||||
/// Default BLE scan interval in seconds.
|
||||
const DEFAULT_BLE_SCAN_INTERVAL_SECS: u64 = 10;
|
||||
|
||||
/// Default BLE beacon interval in seconds.
|
||||
const DEFAULT_BLE_BEACON_INTERVAL_SECS: u64 = 30;
|
||||
|
||||
/// Default BLE beacon duration in seconds (how long each burst lasts).
|
||||
const DEFAULT_BLE_BEACON_DURATION_SECS: u64 = 1;
|
||||
|
||||
/// BLE transport instance configuration.
|
||||
///
|
||||
/// BleConfig is always compiled (for config parsing on any platform),
|
||||
/// but the transport runtime requires Linux and the `ble` feature.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct BleConfig {
|
||||
/// HCI adapter name (e.g., "hci0"). Required.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub adapter: Option<String>,
|
||||
|
||||
/// L2CAP PSM for FIPS connections. Default: 0x0085 (133).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub psm: Option<u16>,
|
||||
|
||||
/// Default MTU for BLE connections. Default: 2048.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub mtu: Option<u16>,
|
||||
|
||||
/// Maximum concurrent BLE connections. Default: 7.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub max_connections: Option<usize>,
|
||||
|
||||
/// Outbound connect timeout in milliseconds. Default: 10000.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub connect_timeout_ms: Option<u64>,
|
||||
|
||||
/// Broadcast BLE advertisements. Default: true.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub advertise: Option<bool>,
|
||||
|
||||
/// Listen for BLE advertisements. Default: true.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub scan: Option<bool>,
|
||||
|
||||
/// Auto-connect to discovered BLE peers. Default: false.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub auto_connect: Option<bool>,
|
||||
|
||||
/// Accept incoming BLE connections. Default: true.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub accept_connections: Option<bool>,
|
||||
|
||||
/// Scan interval in seconds. Default: 10.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub scan_interval_secs: Option<u64>,
|
||||
|
||||
/// Beacon interval in seconds between advertising bursts. Default: 10.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub beacon_interval_secs: Option<u64>,
|
||||
|
||||
/// Beacon duration in seconds per advertising burst. Default: 3.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub beacon_duration_secs: Option<u64>,
|
||||
}
|
||||
|
||||
impl BleConfig {
|
||||
/// Get the adapter name. Default: "hci0".
|
||||
pub fn adapter(&self) -> &str {
|
||||
self.adapter.as_deref().unwrap_or("hci0")
|
||||
}
|
||||
|
||||
/// Get the L2CAP PSM. Default: 0x0085.
|
||||
pub fn psm(&self) -> u16 {
|
||||
self.psm.unwrap_or(DEFAULT_BLE_PSM)
|
||||
}
|
||||
|
||||
/// Get the default MTU. Default: 2048.
|
||||
pub fn mtu(&self) -> u16 {
|
||||
self.mtu.unwrap_or(DEFAULT_BLE_MTU)
|
||||
}
|
||||
|
||||
/// Get the maximum concurrent connections. Default: 7.
|
||||
pub fn max_connections(&self) -> usize {
|
||||
self.max_connections.unwrap_or(DEFAULT_BLE_MAX_CONNECTIONS)
|
||||
}
|
||||
|
||||
/// Get the connect timeout in milliseconds. Default: 10000.
|
||||
pub fn connect_timeout_ms(&self) -> u64 {
|
||||
self.connect_timeout_ms
|
||||
.unwrap_or(DEFAULT_BLE_CONNECT_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
/// Whether to broadcast advertisements. Default: true.
|
||||
pub fn advertise(&self) -> bool {
|
||||
self.advertise.unwrap_or(true)
|
||||
}
|
||||
|
||||
/// Whether to scan for advertisements. Default: true.
|
||||
pub fn scan(&self) -> bool {
|
||||
self.scan.unwrap_or(true)
|
||||
}
|
||||
|
||||
/// Whether to auto-connect to discovered peers. Default: false.
|
||||
pub fn auto_connect(&self) -> bool {
|
||||
self.auto_connect.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Whether to accept incoming connections. Default: true.
|
||||
pub fn accept_connections(&self) -> bool {
|
||||
self.accept_connections.unwrap_or(true)
|
||||
}
|
||||
|
||||
/// Get the scan interval in seconds. Default: 10.
|
||||
pub fn scan_interval_secs(&self) -> u64 {
|
||||
self.scan_interval_secs
|
||||
.unwrap_or(DEFAULT_BLE_SCAN_INTERVAL_SECS)
|
||||
}
|
||||
|
||||
/// Get the beacon interval in seconds. Default: 10.
|
||||
pub fn beacon_interval_secs(&self) -> u64 {
|
||||
self.beacon_interval_secs
|
||||
.unwrap_or(DEFAULT_BLE_BEACON_INTERVAL_SECS)
|
||||
}
|
||||
|
||||
/// Get the beacon duration in seconds. Default: 3.
|
||||
pub fn beacon_duration_secs(&self) -> u64 {
|
||||
self.beacon_duration_secs
|
||||
.unwrap_or(DEFAULT_BLE_BEACON_DURATION_SECS)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TransportsConfig
|
||||
// ============================================================================
|
||||
@@ -523,6 +670,10 @@ pub struct TransportsConfig {
|
||||
/// Tor transport instances.
|
||||
#[serde(default, skip_serializing_if = "is_transport_empty")]
|
||||
pub tor: TransportInstances<TorConfig>,
|
||||
|
||||
/// BLE transport instances.
|
||||
#[serde(default, skip_serializing_if = "is_transport_empty")]
|
||||
pub ble: TransportInstances<BleConfig>,
|
||||
}
|
||||
|
||||
/// Helper for skip_serializing_if on TransportInstances.
|
||||
@@ -533,7 +684,11 @@ fn is_transport_empty<T>(instances: &TransportInstances<T>) -> bool {
|
||||
impl TransportsConfig {
|
||||
/// Check if any transports are configured.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.udp.is_empty() && self.ethernet.is_empty() && self.tcp.is_empty() && self.tor.is_empty()
|
||||
self.udp.is_empty()
|
||||
&& self.ethernet.is_empty()
|
||||
&& self.tcp.is_empty()
|
||||
&& self.tor.is_empty()
|
||||
&& self.ble.is_empty()
|
||||
}
|
||||
|
||||
/// Merge another TransportsConfig into this one.
|
||||
@@ -552,5 +707,8 @@ impl TransportsConfig {
|
||||
if !other.tor.is_empty() {
|
||||
self.tor = other.tor;
|
||||
}
|
||||
if !other.ble.is_empty() {
|
||||
self.ble = other.ble;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
-1
@@ -118,6 +118,30 @@ impl Node {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else if addr.transport == "ble" {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
match self.resolve_ble_addr(&addr.addr) {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
debug!(
|
||||
transport = %addr.transport,
|
||||
addr = %addr.addr,
|
||||
error = %e,
|
||||
"Failed to resolve BLE address"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
debug!(
|
||||
transport = %addr.transport,
|
||||
"BLE transport not available on this platform"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Find a transport matching this address type
|
||||
let tid = match self.find_transport_for_type(&addr.transport) {
|
||||
@@ -513,7 +537,7 @@ impl Node {
|
||||
self.packet_rx = Some(packet_rx);
|
||||
|
||||
// Initialize transports first (before TUN)
|
||||
let transport_handles = self.create_transports(&packet_tx);
|
||||
let transport_handles = self.create_transports(&packet_tx).await;
|
||||
|
||||
for mut handle in transport_handles {
|
||||
let transport_id = handle.transport_id();
|
||||
|
||||
+82
-1
@@ -675,7 +675,7 @@ impl Node {
|
||||
/// Create transport instances from configuration.
|
||||
///
|
||||
/// Returns a vector of TransportHandles for all configured transports.
|
||||
fn create_transports(&mut self, packet_tx: &PacketTx) -> Vec<TransportHandle> {
|
||||
async fn create_transports(&mut self, packet_tx: &PacketTx) -> Vec<TransportHandle> {
|
||||
let mut transports = Vec::new();
|
||||
|
||||
// Collect UDP configs with optional names to avoid borrow conflicts
|
||||
@@ -744,6 +744,47 @@ impl Node {
|
||||
transports.push(TransportHandle::Tor(tor));
|
||||
}
|
||||
|
||||
// Create BLE transport instances
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let ble_instances: Vec<_> = self
|
||||
.config
|
||||
.transports
|
||||
.ble
|
||||
.iter()
|
||||
.map(|(name, config)| (name.map(|s| s.to_string()), config.clone()))
|
||||
.collect();
|
||||
|
||||
#[cfg(all(feature = "ble", not(test)))]
|
||||
for (name, ble_config) in ble_instances {
|
||||
let transport_id = self.allocate_transport_id();
|
||||
let adapter = ble_config.adapter().to_string();
|
||||
let mtu = ble_config.mtu();
|
||||
match crate::transport::ble::io::BluerIo::new(&adapter, mtu).await {
|
||||
Ok(io) => {
|
||||
let mut ble = crate::transport::ble::BleTransport::new(
|
||||
transport_id,
|
||||
name,
|
||||
ble_config,
|
||||
io,
|
||||
packet_tx.clone(),
|
||||
);
|
||||
ble.set_local_pubkey(self.identity.pubkey().serialize());
|
||||
transports.push(TransportHandle::Ble(ble));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(adapter = %adapter, error = %e, "failed to initialize BLE adapter");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(not(feature = "ble"), test))]
|
||||
if !ble_instances.is_empty() {
|
||||
#[cfg(not(test))]
|
||||
tracing::warn!("BLE transport configured but 'ble' feature not enabled at compile time");
|
||||
}
|
||||
}
|
||||
|
||||
transports
|
||||
}
|
||||
|
||||
@@ -806,6 +847,46 @@ impl Node {
|
||||
Ok((transport_id, TransportAddr::from_bytes(&mac)))
|
||||
}
|
||||
|
||||
/// Resolve a BLE address string (`"adapter/AA:BB:CC:DD:EE:FF"`) to a
|
||||
/// (TransportId, TransportAddr) pair by finding the BLE transport
|
||||
/// instance matching the adapter name.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn resolve_ble_addr(
|
||||
&self,
|
||||
addr_str: &str,
|
||||
) -> Result<(TransportId, TransportAddr), NodeError> {
|
||||
let ta = TransportAddr::from_string(addr_str);
|
||||
let adapter = crate::transport::ble::addr::adapter_from_addr(&ta)
|
||||
.ok_or_else(|| {
|
||||
NodeError::NoTransportForType(format!(
|
||||
"invalid BLE address format '{}': expected 'adapter/mac'",
|
||||
addr_str
|
||||
))
|
||||
})?;
|
||||
|
||||
// Find the BLE transport for this adapter
|
||||
let transport_id = self
|
||||
.transports
|
||||
.iter()
|
||||
.find(|(_, handle)| {
|
||||
handle.transport_type().name == "ble" && handle.is_operational()
|
||||
})
|
||||
.map(|(id, _)| *id)
|
||||
.ok_or_else(|| {
|
||||
NodeError::NoTransportForType(format!(
|
||||
"no operational BLE transport for adapter '{}'",
|
||||
adapter
|
||||
))
|
||||
})?;
|
||||
|
||||
// Validate the address format
|
||||
crate::transport::ble::addr::BleAddr::parse(addr_str).map_err(|e| {
|
||||
NodeError::NoTransportForType(format!("invalid BLE address '{}': {}", addr_str, e))
|
||||
})?;
|
||||
|
||||
Ok((transport_id, TransportAddr::from_string(addr_str)))
|
||||
}
|
||||
|
||||
// === Identity Accessors ===
|
||||
|
||||
/// Get this node's identity.
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
//! BLE transport integration tests.
|
||||
//!
|
||||
//! Tests that the BLE transport works end-to-end at the node level:
|
||||
//! handshake, spanning tree convergence, mixed-transport routing.
|
||||
//! All tests use MockBleIo (in-memory channels, no hardware needed).
|
||||
|
||||
use super::*;
|
||||
use crate::config::BleConfig;
|
||||
use crate::transport::ble::addr::BleAddr;
|
||||
use crate::transport::ble::io::{MockBleIo, MockBleStream};
|
||||
use crate::transport::ble::BleTransport;
|
||||
use crate::transport::{packet_channel, Transport, TransportHandle, TransportId};
|
||||
use spanning_tree::{
|
||||
cleanup_nodes, drain_all_packets, initiate_handshake, verify_tree_convergence, TestNode,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex as StdMutex};
|
||||
|
||||
/// Generate a deterministic BLE address for test node `n`.
|
||||
fn ble_addr(n: u8) -> BleAddr {
|
||||
BleAddr {
|
||||
adapter: "hci0".to_string(),
|
||||
device: [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, n],
|
||||
}
|
||||
}
|
||||
|
||||
/// A pre-connected stream bank for MockBleIo connect handlers.
|
||||
///
|
||||
/// When a connect handler fires, it looks up the target address in this
|
||||
/// bank and returns the pre-created stream. The peer end should be
|
||||
/// injected into the target node's acceptor separately.
|
||||
type StreamBank = Arc<StdMutex<HashMap<String, MockBleStream>>>;
|
||||
|
||||
/// Create a test node with a BLE transport backed by MockBleIo.
|
||||
///
|
||||
/// Returns the TestNode and its MockBleIo (via Arc inside the transport)
|
||||
/// for test injection of connections and scan results.
|
||||
async fn make_test_node_ble(node_num: u8) -> TestNode {
|
||||
let mut node = make_node();
|
||||
let transport_id = TransportId::new(1);
|
||||
let addr = ble_addr(node_num);
|
||||
|
||||
let config = BleConfig {
|
||||
adapter: Some("hci0".to_string()),
|
||||
mtu: Some(2048),
|
||||
accept_connections: Some(true),
|
||||
scan: Some(false), // no auto-scan in tests
|
||||
advertise: Some(false), // no advertising in tests
|
||||
auto_connect: Some(false),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let io = MockBleIo::new("hci0", addr.clone());
|
||||
let (packet_tx, packet_rx) = packet_channel(256);
|
||||
let mut transport = BleTransport::new(transport_id, None, config, io, packet_tx);
|
||||
transport.start_async().await.unwrap();
|
||||
|
||||
let ta = addr.to_transport_addr();
|
||||
|
||||
node.transports
|
||||
.insert(transport_id, TransportHandle::Ble(transport));
|
||||
|
||||
TestNode {
|
||||
node,
|
||||
transport_id,
|
||||
packet_rx,
|
||||
addr: ta,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the BleAddr from a TestNode's TransportAddr.
|
||||
fn node_ble_addr(node: &TestNode) -> BleAddr {
|
||||
BleAddr::parse(node.addr.as_str().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
/// Wire a unidirectional BLE connection from node `i` to node `j`.
|
||||
///
|
||||
/// Creates a MockBleStream pair, deposits one end in a stream bank for
|
||||
/// node i's connect handler, and injects the other end into node j's
|
||||
/// accept loop. Must be called after `make_test_node_ble()` and before
|
||||
/// `initiate_handshake()`.
|
||||
async fn wire_ble_connection(nodes: &[TestNode], i: usize, j: usize, bank: &StreamBank) {
|
||||
let addr_i = node_ble_addr(&nodes[i]);
|
||||
let addr_j = node_ble_addr(&nodes[j]);
|
||||
|
||||
let (stream_i, stream_j) = MockBleStream::pair(addr_j.clone(), addr_i.clone(), 2048);
|
||||
|
||||
// Store stream_i in the bank keyed by node j's address string.
|
||||
// When node i connects to node j, the handler returns this stream.
|
||||
let key = nodes[j].addr.to_string();
|
||||
bank.lock().unwrap().insert(key, stream_i);
|
||||
|
||||
// Inject stream_j into node j's accept loop so it sees the inbound.
|
||||
let transport_j = nodes[j].node.transports.get(&nodes[j].transport_id).unwrap();
|
||||
match transport_j {
|
||||
TransportHandle::Ble(t) => {
|
||||
t.io().inject_inbound(stream_j).await;
|
||||
}
|
||||
_ => panic!("expected BLE transport"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Install a connect handler on node `i` that draws from the stream bank.
|
||||
fn install_connect_handler(nodes: &[TestNode], i: usize, bank: &StreamBank) {
|
||||
let bank = Arc::clone(bank);
|
||||
let transport_i = nodes[i].node.transports.get(&nodes[i].transport_id).unwrap();
|
||||
match transport_i {
|
||||
TransportHandle::Ble(t) => {
|
||||
t.io().set_connect_handler(move |addr, _psm| {
|
||||
let key = addr.to_transport_addr().to_string();
|
||||
let mut map = bank.lock().unwrap();
|
||||
match map.remove(&key) {
|
||||
Some(stream) => Ok(stream),
|
||||
None => Err(crate::transport::TransportError::ConnectionRefused),
|
||||
}
|
||||
});
|
||||
}
|
||||
_ => panic!("expected BLE transport"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Two BLE nodes complete a Noise handshake and establish bidirectional peering.
|
||||
#[tokio::test]
|
||||
async fn test_ble_two_node_handshake() {
|
||||
let mut nodes = vec![make_test_node_ble(1).await, make_test_node_ble(2).await];
|
||||
|
||||
// Wire connection: node 0 → node 1
|
||||
let bank: StreamBank = Arc::new(StdMutex::new(HashMap::new()));
|
||||
wire_ble_connection(&nodes, 0, 1, &bank).await;
|
||||
install_connect_handler(&nodes, 0, &bank);
|
||||
|
||||
// Initiate handshake (connect-on-send creates the BLE connection)
|
||||
initiate_handshake(&mut nodes, 0, 1).await;
|
||||
|
||||
// Drain all packets (handshake + TreeAnnounce exchange)
|
||||
let total = drain_all_packets(&mut nodes, false).await;
|
||||
assert!(total > 0, "should have processed packets");
|
||||
|
||||
// Verify bidirectional peering
|
||||
let addr_0 = *nodes[0].node.node_addr();
|
||||
let addr_1 = *nodes[1].node.node_addr();
|
||||
assert!(
|
||||
nodes[0].node.get_peer(&addr_1).is_some(),
|
||||
"node 0 should have node 1 as peer"
|
||||
);
|
||||
assert!(
|
||||
nodes[1].node.get_peer(&addr_0).is_some(),
|
||||
"node 1 should have node 0 as peer"
|
||||
);
|
||||
|
||||
cleanup_nodes(&mut nodes).await;
|
||||
}
|
||||
|
||||
/// Three BLE nodes in a chain converge to a consistent spanning tree.
|
||||
#[tokio::test]
|
||||
async fn test_ble_three_node_chain() {
|
||||
let mut nodes = vec![
|
||||
make_test_node_ble(1).await,
|
||||
make_test_node_ble(2).await,
|
||||
make_test_node_ble(3).await,
|
||||
];
|
||||
|
||||
let bank: StreamBank = Arc::new(StdMutex::new(HashMap::new()));
|
||||
|
||||
// Wire: 0 -- 1 -- 2
|
||||
wire_ble_connection(&nodes, 0, 1, &bank).await;
|
||||
wire_ble_connection(&nodes, 1, 2, &bank).await;
|
||||
install_connect_handler(&nodes, 0, &bank);
|
||||
install_connect_handler(&nodes, 1, &bank);
|
||||
|
||||
initiate_handshake(&mut nodes, 0, 1).await;
|
||||
initiate_handshake(&mut nodes, 1, 2).await;
|
||||
|
||||
let total = drain_all_packets(&mut nodes, false).await;
|
||||
assert!(total > 0, "should have processed packets");
|
||||
|
||||
// Verify spanning tree convergence
|
||||
verify_tree_convergence(&nodes);
|
||||
|
||||
// Verify correct root
|
||||
let expected_root = nodes.iter().map(|tn| *tn.node.node_addr()).min().unwrap();
|
||||
for tn in &nodes {
|
||||
assert_eq!(*tn.node.tree_state().root(), expected_root);
|
||||
}
|
||||
|
||||
// Verify peer counts
|
||||
assert_eq!(nodes[0].node.peer_count(), 1);
|
||||
assert_eq!(nodes[1].node.peer_count(), 2);
|
||||
assert_eq!(nodes[2].node.peer_count(), 1);
|
||||
|
||||
// Verify bloom filter reachability: node 0 → node 2
|
||||
let addr_2 = *nodes[2].node.node_addr();
|
||||
let reaches = nodes[0].node.peers().any(|p| p.may_reach(&addr_2));
|
||||
assert!(reaches, "node 0 should see node 2 as reachable");
|
||||
|
||||
cleanup_nodes(&mut nodes).await;
|
||||
}
|
||||
|
||||
/// Mixed transport: UDP and BLE nodes coexist in independent components.
|
||||
#[tokio::test]
|
||||
async fn test_ble_mixed_transport() {
|
||||
use spanning_tree::{make_test_node, verify_tree_convergence_components};
|
||||
|
||||
let udp_0 = make_test_node().await;
|
||||
let udp_1 = make_test_node().await;
|
||||
let ble_0 = make_test_node_ble(1).await;
|
||||
let ble_1 = make_test_node_ble(2).await;
|
||||
|
||||
let mut nodes = vec![udp_0, udp_1, ble_0, ble_1];
|
||||
|
||||
// Wire BLE pair
|
||||
let bank: StreamBank = Arc::new(StdMutex::new(HashMap::new()));
|
||||
wire_ble_connection(&nodes, 2, 3, &bank).await;
|
||||
install_connect_handler(&nodes, 2, &bank);
|
||||
|
||||
// Handshake within each component
|
||||
initiate_handshake(&mut nodes, 0, 1).await; // UDP pair
|
||||
initiate_handshake(&mut nodes, 2, 3).await; // BLE pair
|
||||
|
||||
let total = drain_all_packets(&mut nodes, false).await;
|
||||
assert!(total > 0);
|
||||
|
||||
// Verify each component converges independently
|
||||
verify_tree_convergence_components(&nodes, &[vec![0, 1], vec![2, 3]]);
|
||||
|
||||
// BLE component has its own root
|
||||
let ble_root = std::cmp::min(*nodes[2].node.node_addr(), *nodes[3].node.node_addr());
|
||||
assert_eq!(*nodes[2].node.tree_state().root(), ble_root);
|
||||
assert_eq!(*nodes[3].node.tree_state().root(), ble_root);
|
||||
|
||||
cleanup_nodes(&mut nodes).await;
|
||||
}
|
||||
|
||||
/// BLE scan+probe loop discovers peers via adapter scan events.
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn test_ble_discovery() {
|
||||
let mut node = make_node();
|
||||
let transport_id = TransportId::new(1);
|
||||
let addr = ble_addr(1);
|
||||
|
||||
// Enable scanning so the scan+probe loop runs
|
||||
let config = BleConfig {
|
||||
adapter: Some("hci0".to_string()),
|
||||
mtu: Some(2048),
|
||||
accept_connections: Some(true),
|
||||
scan: Some(true),
|
||||
advertise: Some(false),
|
||||
auto_connect: Some(false),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let io = MockBleIo::new("hci0", addr.clone());
|
||||
let (packet_tx, packet_rx) = packet_channel(256);
|
||||
let mut transport = BleTransport::new(transport_id, None, config, io, packet_tx);
|
||||
transport.start_async().await.unwrap();
|
||||
|
||||
// Inject scan results via the I/O mock
|
||||
transport.io().inject_scan_result(ble_addr(2)).await;
|
||||
transport.io().inject_scan_result(ble_addr(3)).await;
|
||||
|
||||
// Let scan_probe_loop pick up results and schedule jitter
|
||||
tokio::task::yield_now().await;
|
||||
// Advance past max jitter so probes fire
|
||||
tokio::time::advance(std::time::Duration::from_secs(6)).await;
|
||||
tokio::task::yield_now().await;
|
||||
|
||||
// Without pubkey set, peers appear as bare MACs in discovery buffer
|
||||
let peers = transport.discover().unwrap();
|
||||
assert_eq!(peers.len(), 2);
|
||||
|
||||
let ta = addr.to_transport_addr();
|
||||
node.transports
|
||||
.insert(transport_id, TransportHandle::Ble(transport));
|
||||
|
||||
let mut nodes = vec![TestNode { node, transport_id, packet_rx, addr: ta }];
|
||||
cleanup_nodes(&mut nodes).await;
|
||||
}
|
||||
@@ -5,6 +5,8 @@ use crate::PeerIdentity;
|
||||
use std::time::Duration;
|
||||
|
||||
mod bloom;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod ble;
|
||||
mod disconnect;
|
||||
mod discovery;
|
||||
#[cfg(target_os = "linux")]
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
//! BLE transport address parsing and formatting.
|
||||
//!
|
||||
//! Address format: `"hci0/AA:BB:CC:DD:EE:FF"` — adapter name / device address.
|
||||
|
||||
use crate::transport::{TransportAddr, TransportError};
|
||||
|
||||
/// A parsed BLE device address.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct BleAddr {
|
||||
/// HCI adapter name (e.g., "hci0").
|
||||
pub adapter: String,
|
||||
/// 6-byte Bluetooth device address.
|
||||
pub device: [u8; 6],
|
||||
}
|
||||
|
||||
impl BleAddr {
|
||||
/// Parse a BLE address from the `"adapter/AA:BB:CC:DD:EE:FF"` format.
|
||||
pub fn parse(s: &str) -> Result<Self, TransportError> {
|
||||
let (adapter, mac_str) = s
|
||||
.split_once('/')
|
||||
.ok_or_else(|| TransportError::InvalidAddress(format!("missing '/' in BLE address: {s}")))?;
|
||||
|
||||
if adapter.is_empty() {
|
||||
return Err(TransportError::InvalidAddress("empty adapter name".into()));
|
||||
}
|
||||
|
||||
let device = parse_mac(mac_str).ok_or_else(|| {
|
||||
TransportError::InvalidAddress(format!("invalid MAC address: {mac_str}"))
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
adapter: adapter.to_string(),
|
||||
device,
|
||||
})
|
||||
}
|
||||
|
||||
/// Format as `"adapter/AA:BB:CC:DD:EE:FF"`.
|
||||
pub fn to_string_repr(&self) -> String {
|
||||
format!(
|
||||
"{}/{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
|
||||
self.adapter,
|
||||
self.device[0],
|
||||
self.device[1],
|
||||
self.device[2],
|
||||
self.device[3],
|
||||
self.device[4],
|
||||
self.device[5],
|
||||
)
|
||||
}
|
||||
|
||||
/// Convert to a `TransportAddr` (string representation).
|
||||
pub fn to_transport_addr(&self) -> TransportAddr {
|
||||
TransportAddr::from_string(&self.to_string_repr())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// bluer type conversions (behind ble feature)
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(feature = "ble")]
|
||||
impl BleAddr {
|
||||
/// Construct from a bluer `Address` and adapter name.
|
||||
pub fn from_bluer(addr: bluer::Address, adapter: &str) -> Self {
|
||||
Self {
|
||||
adapter: adapter.to_string(),
|
||||
device: addr.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert to a bluer `Address`.
|
||||
pub fn to_bluer_address(&self) -> bluer::Address {
|
||||
bluer::Address(self.device)
|
||||
}
|
||||
|
||||
/// Convert to a bluer L2CAP `SocketAddr` with the given PSM.
|
||||
pub fn to_socket_addr(&self, psm: u16) -> bluer::l2cap::SocketAddr {
|
||||
bluer::l2cap::SocketAddr::new(
|
||||
self.to_bluer_address(),
|
||||
bluer::AddressType::LePublic,
|
||||
psm,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for BleAddr {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.to_string_repr())
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a colon-delimited MAC address string into 6 bytes.
|
||||
fn parse_mac(s: &str) -> Option<[u8; 6]> {
|
||||
let parts: Vec<&str> = s.split(':').collect();
|
||||
if parts.len() != 6 {
|
||||
return None;
|
||||
}
|
||||
let mut mac = [0u8; 6];
|
||||
for (i, part) in parts.iter().enumerate() {
|
||||
mac[i] = u8::from_str_radix(part, 16).ok()?;
|
||||
}
|
||||
Some(mac)
|
||||
}
|
||||
|
||||
/// Extract the adapter name from a transport address string.
|
||||
///
|
||||
/// Returns `None` if the address is not valid UTF-8 or doesn't contain '/'.
|
||||
pub fn adapter_from_addr(addr: &TransportAddr) -> Option<&str> {
|
||||
addr.as_str()?.split_once('/').map(|(adapter, _)| adapter)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_valid() {
|
||||
let addr = BleAddr::parse("hci0/AA:BB:CC:DD:EE:FF").unwrap();
|
||||
assert_eq!(addr.adapter, "hci0");
|
||||
assert_eq!(addr.device, [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_lowercase() {
|
||||
let addr = BleAddr::parse("hci1/aa:bb:cc:dd:ee:ff").unwrap();
|
||||
assert_eq!(addr.adapter, "hci1");
|
||||
assert_eq!(addr.device, [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip() {
|
||||
let original = "hci0/AA:BB:CC:DD:EE:FF";
|
||||
let addr = BleAddr::parse(original).unwrap();
|
||||
assert_eq!(addr.to_string_repr(), original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
let addr = BleAddr::parse("hci0/01:02:03:04:05:06").unwrap();
|
||||
assert_eq!(format!("{addr}"), "hci0/01:02:03:04:05:06");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_transport_addr() {
|
||||
let addr = BleAddr::parse("hci0/AA:BB:CC:DD:EE:FF").unwrap();
|
||||
let ta = addr.to_transport_addr();
|
||||
assert_eq!(ta.as_str(), Some("hci0/AA:BB:CC:DD:EE:FF"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_missing_slash() {
|
||||
assert!(BleAddr::parse("hci0-AA:BB:CC:DD:EE:FF").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_empty_adapter() {
|
||||
assert!(BleAddr::parse("/AA:BB:CC:DD:EE:FF").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_invalid_mac_short() {
|
||||
assert!(BleAddr::parse("hci0/AA:BB:CC").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_invalid_mac_hex() {
|
||||
assert!(BleAddr::parse("hci0/GG:HH:II:JJ:KK:LL").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adapter_from_addr() {
|
||||
let ta = TransportAddr::from_string("hci0/AA:BB:CC:DD:EE:FF");
|
||||
assert_eq!(adapter_from_addr(&ta), Some("hci0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adapter_from_addr_no_slash() {
|
||||
let ta = TransportAddr::from_string("invalid");
|
||||
assert_eq!(adapter_from_addr(&ta), None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
//! BLE discovery via advertising and scanning.
|
||||
//!
|
||||
//! BLE advertisements carry a 128-bit FIPS service UUID for identification.
|
||||
//! Post-forklift, advertisements are UUID-only (no identity material);
|
||||
//! identity is exchanged during the Noise handshake.
|
||||
|
||||
use crate::transport::{DiscoveredPeer, TransportId};
|
||||
use secp256k1::XOnlyPublicKey;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use super::addr::BleAddr;
|
||||
|
||||
/// Buffer for discovered BLE peers, drained by `discover()`.
|
||||
///
|
||||
/// Follows the same pattern as Ethernet's `DiscoveryBuffer`: peers are
|
||||
/// added from the scan loop and drained by the node's discovery polling.
|
||||
pub struct DiscoveryBuffer {
|
||||
transport_id: TransportId,
|
||||
peers: Mutex<Vec<DiscoveredPeer>>,
|
||||
}
|
||||
|
||||
impl DiscoveryBuffer {
|
||||
/// Create a new empty discovery buffer.
|
||||
pub fn new(transport_id: TransportId) -> Self {
|
||||
Self {
|
||||
transport_id,
|
||||
peers: Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a discovered BLE peer.
|
||||
///
|
||||
/// Deduplicates by device address — keeps the latest entry.
|
||||
pub fn add_peer(&self, addr: &BleAddr) {
|
||||
let ta = addr.to_transport_addr();
|
||||
let peer = DiscoveredPeer::new(self.transport_id, ta.clone());
|
||||
let mut peers = self.peers.lock().unwrap();
|
||||
// Deduplicate by address string
|
||||
let addr_str = addr.to_string_repr();
|
||||
peers.retain(|p| p.addr.as_str() != Some(addr_str.as_str()));
|
||||
peers.push(peer);
|
||||
}
|
||||
|
||||
/// Add a discovered BLE peer with a known public key.
|
||||
///
|
||||
/// Used after the pre-handshake pubkey exchange confirms the peer's
|
||||
/// identity. The pubkey_hint enables the node's auto-connect path
|
||||
/// to initiate the IK handshake.
|
||||
pub fn add_peer_with_pubkey(&self, addr: &BleAddr, pubkey: XOnlyPublicKey) {
|
||||
let ta = addr.to_transport_addr();
|
||||
let peer = DiscoveredPeer::with_hint(self.transport_id, ta.clone(), pubkey);
|
||||
let mut peers = self.peers.lock().unwrap();
|
||||
let addr_str = addr.to_string_repr();
|
||||
peers.retain(|p| p.addr.as_str() != Some(addr_str.as_str()));
|
||||
peers.push(peer);
|
||||
}
|
||||
|
||||
/// Drain all discovered peers since the last call.
|
||||
pub fn take(&self) -> Vec<DiscoveredPeer> {
|
||||
let mut peers = self.peers.lock().unwrap();
|
||||
std::mem::take(&mut *peers)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::transport::TransportAddr;
|
||||
|
||||
fn test_addr(n: u8) -> BleAddr {
|
||||
BleAddr {
|
||||
adapter: "hci0".to_string(),
|
||||
device: [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, n],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_discovery_buffer_add_take() {
|
||||
let buffer = DiscoveryBuffer::new(TransportId::new(1));
|
||||
buffer.add_peer(&test_addr(1));
|
||||
|
||||
let peers = buffer.take();
|
||||
assert_eq!(peers.len(), 1);
|
||||
|
||||
// Second take should be empty
|
||||
let peers = buffer.take();
|
||||
assert!(peers.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_discovery_buffer_dedup() {
|
||||
let buffer = DiscoveryBuffer::new(TransportId::new(1));
|
||||
buffer.add_peer(&test_addr(1));
|
||||
buffer.add_peer(&test_addr(1)); // same address again
|
||||
|
||||
let peers = buffer.take();
|
||||
assert_eq!(peers.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_discovery_buffer_multiple_peers() {
|
||||
let buffer = DiscoveryBuffer::new(TransportId::new(1));
|
||||
buffer.add_peer(&test_addr(1));
|
||||
buffer.add_peer(&test_addr(2));
|
||||
buffer.add_peer(&test_addr(3));
|
||||
|
||||
let peers = buffer.take();
|
||||
assert_eq!(peers.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_discovery_buffer_transport_addr_format() {
|
||||
let buffer = DiscoveryBuffer::new(TransportId::new(1));
|
||||
buffer.add_peer(&test_addr(0x42));
|
||||
|
||||
let peers = buffer.take();
|
||||
assert_eq!(
|
||||
peers[0].addr,
|
||||
TransportAddr::from_string("hci0/AA:BB:CC:DD:EE:42")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,774 @@
|
||||
//! BLE I/O abstraction layer.
|
||||
//!
|
||||
//! Defines the `BleIo` trait that separates transport logic from the
|
||||
//! BlueZ/bluer stack. `BluerIo` (behind `cfg(feature = "ble")`) provides
|
||||
//! the real implementation; `MockBleIo` provides an in-memory test double.
|
||||
|
||||
use crate::transport::TransportError;
|
||||
|
||||
use super::addr::BleAddr;
|
||||
|
||||
// ============================================================================
|
||||
// BLE I/O Traits
|
||||
// ============================================================================
|
||||
|
||||
/// A connected L2CAP stream for sending and receiving data.
|
||||
pub trait BleStream: Send + Sync {
|
||||
/// Send data over the L2CAP connection.
|
||||
fn send(
|
||||
&self,
|
||||
data: &[u8],
|
||||
) -> impl std::future::Future<Output = Result<(), TransportError>> + Send;
|
||||
|
||||
/// Receive data from the L2CAP connection.
|
||||
///
|
||||
/// Returns the number of bytes read into `buf`.
|
||||
fn recv(
|
||||
&self,
|
||||
buf: &mut [u8],
|
||||
) -> impl std::future::Future<Output = Result<usize, TransportError>> + Send;
|
||||
|
||||
/// Get the L2CAP send MTU for this connection.
|
||||
fn send_mtu(&self) -> u16;
|
||||
|
||||
/// Get the L2CAP receive MTU for this connection.
|
||||
fn recv_mtu(&self) -> u16;
|
||||
|
||||
/// Get the remote device address.
|
||||
fn remote_addr(&self) -> &BleAddr;
|
||||
}
|
||||
|
||||
/// An acceptor that yields inbound L2CAP connections.
|
||||
pub trait BleAcceptor: Send {
|
||||
/// The concrete stream type yielded by this acceptor.
|
||||
type Stream: BleStream + 'static;
|
||||
|
||||
/// Accept the next inbound connection.
|
||||
fn accept(
|
||||
&mut self,
|
||||
) -> impl std::future::Future<Output = Result<Self::Stream, TransportError>> + Send;
|
||||
}
|
||||
|
||||
/// A scanner that yields discovered BLE devices advertising the FIPS UUID.
|
||||
pub trait BleScanner: Send {
|
||||
/// Wait for the next discovered device.
|
||||
///
|
||||
/// Returns `None` when scanning is stopped.
|
||||
fn next(
|
||||
&mut self,
|
||||
) -> impl std::future::Future<Output = Option<BleAddr>> + Send;
|
||||
}
|
||||
|
||||
/// Core BLE I/O operations.
|
||||
///
|
||||
/// This trait abstracts the BlueZ/bluer stack so that `BleTransport`
|
||||
/// can be tested with `MockBleIo` (in-memory channels) in CI without
|
||||
/// requiring Bluetooth hardware, D-Bus, or bluetoothd.
|
||||
pub trait BleIo: Send + Sync + 'static {
|
||||
/// The concrete stream type returned by this I/O implementation.
|
||||
type Stream: BleStream + 'static;
|
||||
/// The concrete acceptor type.
|
||||
type Acceptor: BleAcceptor<Stream = Self::Stream> + 'static;
|
||||
/// The concrete scanner type.
|
||||
type Scanner: BleScanner + 'static;
|
||||
|
||||
/// Start listening for inbound L2CAP connections on the given PSM.
|
||||
fn listen(
|
||||
&self,
|
||||
psm: u16,
|
||||
) -> impl std::future::Future<Output = Result<Self::Acceptor, TransportError>> + Send;
|
||||
|
||||
/// Connect to a remote BLE device on the given PSM.
|
||||
fn connect(
|
||||
&self,
|
||||
addr: &BleAddr,
|
||||
psm: u16,
|
||||
) -> impl std::future::Future<Output = Result<Self::Stream, TransportError>> + Send;
|
||||
|
||||
/// Start advertising the FIPS service UUID.
|
||||
fn start_advertising(
|
||||
&self,
|
||||
) -> impl std::future::Future<Output = Result<(), TransportError>> + Send;
|
||||
|
||||
/// Stop advertising.
|
||||
fn stop_advertising(
|
||||
&self,
|
||||
) -> impl std::future::Future<Output = Result<(), TransportError>> + Send;
|
||||
|
||||
/// Start passive scanning for FIPS service UUID advertisements.
|
||||
fn start_scanning(
|
||||
&self,
|
||||
) -> impl std::future::Future<Output = Result<Self::Scanner, TransportError>> + Send;
|
||||
|
||||
/// Get the adapter's BLE address.
|
||||
fn local_addr(&self) -> Result<BleAddr, TransportError>;
|
||||
|
||||
/// Get the adapter name (e.g., "hci0").
|
||||
fn adapter_name(&self) -> &str;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// BluerIo — Production BLE I/O via BlueZ D-Bus
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(feature = "ble")]
|
||||
mod bluer_impl {
|
||||
use super::*;
|
||||
use crate::transport::TransportError;
|
||||
|
||||
use bluer::l2cap::{SeqPacket, SeqPacketListener, Socket, SocketAddr};
|
||||
use bluer::{adv::Advertisement, AdapterEvent, AddressType, DiscoveryFilter, DiscoveryTransport};
|
||||
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;
|
||||
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))?;
|
||||
|
||||
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))?;
|
||||
|
||||
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()),
|
||||
..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> {
|
||||
// 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(feature = "ble")]
|
||||
pub use bluer_impl::{BluerAcceptor, BluerIo, BluerScanner, BluerStream, FIPS_SERVICE_UUID};
|
||||
|
||||
// ============================================================================
|
||||
// Mock BLE I/O (for testing without hardware)
|
||||
// ============================================================================
|
||||
|
||||
/// Mock BLE stream backed by tokio channels.
|
||||
pub struct MockBleStream {
|
||||
addr: BleAddr,
|
||||
send_mtu: u16,
|
||||
recv_mtu: u16,
|
||||
tx: tokio::sync::mpsc::Sender<Vec<u8>>,
|
||||
rx: tokio::sync::Mutex<tokio::sync::mpsc::Receiver<Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl MockBleStream {
|
||||
/// Create a linked pair of mock streams simulating an L2CAP connection.
|
||||
pub fn pair(
|
||||
addr_a: BleAddr,
|
||||
addr_b: BleAddr,
|
||||
mtu: u16,
|
||||
) -> (Self, Self) {
|
||||
let (tx_a, rx_a) = tokio::sync::mpsc::channel(64);
|
||||
let (tx_b, rx_b) = tokio::sync::mpsc::channel(64);
|
||||
let stream_a = Self {
|
||||
addr: addr_b.clone(),
|
||||
send_mtu: mtu,
|
||||
recv_mtu: mtu,
|
||||
tx: tx_a,
|
||||
rx: tokio::sync::Mutex::new(rx_b),
|
||||
};
|
||||
let stream_b = Self {
|
||||
addr: addr_a,
|
||||
send_mtu: mtu,
|
||||
recv_mtu: mtu,
|
||||
tx: tx_b,
|
||||
rx: tokio::sync::Mutex::new(rx_a),
|
||||
};
|
||||
(stream_a, stream_b)
|
||||
}
|
||||
}
|
||||
|
||||
impl BleStream for MockBleStream {
|
||||
async fn send(&self, data: &[u8]) -> Result<(), TransportError> {
|
||||
self.tx
|
||||
.send(data.to_vec())
|
||||
.await
|
||||
.map_err(|_| TransportError::SendFailed("channel closed".into()))
|
||||
}
|
||||
|
||||
async fn recv(&self, buf: &mut [u8]) -> Result<usize, TransportError> {
|
||||
let mut rx = self.rx.lock().await;
|
||||
match rx.recv().await {
|
||||
Some(data) => {
|
||||
let len = data.len().min(buf.len());
|
||||
buf[..len].copy_from_slice(&data[..len]);
|
||||
Ok(len)
|
||||
}
|
||||
None => Ok(0), // channel closed = connection closed = zero-length read
|
||||
}
|
||||
}
|
||||
|
||||
fn send_mtu(&self) -> u16 {
|
||||
self.send_mtu
|
||||
}
|
||||
|
||||
fn recv_mtu(&self) -> u16 {
|
||||
self.recv_mtu
|
||||
}
|
||||
|
||||
fn remote_addr(&self) -> &BleAddr {
|
||||
&self.addr
|
||||
}
|
||||
}
|
||||
|
||||
/// Mock BLE acceptor backed by a channel of pre-connected streams.
|
||||
pub struct MockBleAcceptor {
|
||||
rx: tokio::sync::mpsc::Receiver<MockBleStream>,
|
||||
}
|
||||
|
||||
impl BleAcceptor for MockBleAcceptor {
|
||||
type Stream = MockBleStream;
|
||||
|
||||
async fn accept(&mut self) -> Result<MockBleStream, TransportError> {
|
||||
self.rx
|
||||
.recv()
|
||||
.await
|
||||
.ok_or(TransportError::RecvFailed("acceptor channel closed".into()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Mock BLE scanner backed by a channel of discovered addresses.
|
||||
pub struct MockBleScanner {
|
||||
rx: tokio::sync::mpsc::Receiver<BleAddr>,
|
||||
}
|
||||
|
||||
impl BleScanner for MockBleScanner {
|
||||
async fn next(&mut self) -> Option<BleAddr> {
|
||||
self.rx.recv().await
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler type for outbound mock connections.
|
||||
type ConnectHandler =
|
||||
Box<dyn Fn(&BleAddr, u16) -> Result<MockBleStream, TransportError> + Send + Sync>;
|
||||
|
||||
/// Mock BLE I/O for testing without hardware.
|
||||
///
|
||||
/// Create with `MockBleIo::new()`, then use `inject_*` methods to
|
||||
/// feed connections and scan results into the transport under test.
|
||||
pub struct MockBleIo {
|
||||
adapter: String,
|
||||
local_addr: BleAddr,
|
||||
accept_tx: tokio::sync::mpsc::Sender<MockBleStream>,
|
||||
accept_rx: std::sync::Mutex<Option<tokio::sync::mpsc::Receiver<MockBleStream>>>,
|
||||
scan_tx: tokio::sync::mpsc::Sender<BleAddr>,
|
||||
scan_rx: std::sync::Mutex<Option<tokio::sync::mpsc::Receiver<BleAddr>>>,
|
||||
connect_handler: std::sync::Mutex<Option<ConnectHandler>>,
|
||||
}
|
||||
|
||||
impl MockBleIo {
|
||||
/// Create a new mock BLE I/O with the given adapter name and address.
|
||||
pub fn new(adapter: &str, local_addr: BleAddr) -> Self {
|
||||
let (accept_tx, accept_rx) = tokio::sync::mpsc::channel(16);
|
||||
let (scan_tx, scan_rx) = tokio::sync::mpsc::channel(64);
|
||||
Self {
|
||||
adapter: adapter.to_string(),
|
||||
local_addr,
|
||||
accept_tx,
|
||||
accept_rx: std::sync::Mutex::new(Some(accept_rx)),
|
||||
scan_tx,
|
||||
scan_rx: std::sync::Mutex::new(Some(scan_rx)),
|
||||
connect_handler: std::sync::Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Inject an inbound connection (simulates a remote device connecting).
|
||||
pub async fn inject_inbound(&self, stream: MockBleStream) {
|
||||
let _ = self.accept_tx.send(stream).await;
|
||||
}
|
||||
|
||||
/// Inject a scan result (simulates discovering a remote device).
|
||||
pub async fn inject_scan_result(&self, addr: BleAddr) {
|
||||
let _ = self.scan_tx.send(addr).await;
|
||||
}
|
||||
|
||||
/// Set a handler for outbound connect calls.
|
||||
pub fn set_connect_handler<F>(&self, handler: F)
|
||||
where
|
||||
F: Fn(&BleAddr, u16) -> Result<MockBleStream, TransportError> + Send + Sync + 'static,
|
||||
{
|
||||
*self.connect_handler.lock().unwrap() = Some(Box::new(handler));
|
||||
}
|
||||
}
|
||||
|
||||
impl BleIo for MockBleIo {
|
||||
type Stream = MockBleStream;
|
||||
type Acceptor = MockBleAcceptor;
|
||||
type Scanner = MockBleScanner;
|
||||
|
||||
async fn listen(&self, _psm: u16) -> Result<Self::Acceptor, TransportError> {
|
||||
let rx = self
|
||||
.accept_rx
|
||||
.lock()
|
||||
.unwrap()
|
||||
.take()
|
||||
.ok_or_else(|| TransportError::NotSupported("acceptor already taken".into()))?;
|
||||
Ok(MockBleAcceptor { rx })
|
||||
}
|
||||
|
||||
async fn connect(&self, addr: &BleAddr, psm: u16) -> Result<Self::Stream, TransportError> {
|
||||
let handler = self.connect_handler.lock().unwrap();
|
||||
match handler.as_ref() {
|
||||
Some(f) => f(addr, psm),
|
||||
None => Err(TransportError::ConnectionRefused),
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_advertising(&self) -> Result<(), TransportError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stop_advertising(&self) -> Result<(), TransportError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn start_scanning(&self) -> Result<Self::Scanner, TransportError> {
|
||||
let rx = self
|
||||
.scan_rx
|
||||
.lock()
|
||||
.unwrap()
|
||||
.take()
|
||||
.ok_or_else(|| TransportError::NotSupported("scanner already taken".into()))?;
|
||||
Ok(MockBleScanner { rx })
|
||||
}
|
||||
|
||||
fn local_addr(&self) -> Result<BleAddr, TransportError> {
|
||||
Ok(self.local_addr.clone())
|
||||
}
|
||||
|
||||
fn adapter_name(&self) -> &str {
|
||||
&self.adapter
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn test_addr(n: u8) -> BleAddr {
|
||||
BleAddr {
|
||||
adapter: "hci0".to_string(),
|
||||
device: [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, n],
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mock_stream_pair_send_recv() {
|
||||
let (a, b) = MockBleStream::pair(test_addr(1), test_addr(2), 2048);
|
||||
|
||||
a.send(b"hello").await.unwrap();
|
||||
let mut buf = [0u8; 64];
|
||||
let n = b.recv(&mut buf).await.unwrap();
|
||||
assert_eq!(&buf[..n], b"hello");
|
||||
|
||||
b.send(b"world").await.unwrap();
|
||||
let n = a.recv(&mut buf).await.unwrap();
|
||||
assert_eq!(&buf[..n], b"world");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mock_stream_mtu() {
|
||||
let (a, b) = MockBleStream::pair(test_addr(1), test_addr(2), 512);
|
||||
assert_eq!(a.send_mtu(), 512);
|
||||
assert_eq!(a.recv_mtu(), 512);
|
||||
assert_eq!(b.send_mtu(), 512);
|
||||
assert_eq!(b.recv_mtu(), 512);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mock_stream_remote_addr() {
|
||||
let (a, b) = MockBleStream::pair(test_addr(1), test_addr(2), 2048);
|
||||
assert_eq!(a.remote_addr(), &test_addr(2));
|
||||
assert_eq!(b.remote_addr(), &test_addr(1));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mock_io_listen_accept() {
|
||||
let io = MockBleIo::new("hci0", test_addr(1));
|
||||
let mut acceptor = io.listen(0x0085).await.unwrap();
|
||||
|
||||
let (stream_a, _stream_b) = MockBleStream::pair(test_addr(1), test_addr(2), 2048);
|
||||
io.inject_inbound(stream_a).await;
|
||||
|
||||
let accepted = acceptor.accept().await.unwrap();
|
||||
// stream_a's remote_addr is addr_b (test_addr(2))
|
||||
assert_eq!(accepted.remote_addr(), &test_addr(2));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mock_io_connect() {
|
||||
let io = MockBleIo::new("hci0", test_addr(1));
|
||||
let local = test_addr(1);
|
||||
io.set_connect_handler(move |addr, _psm| {
|
||||
let (stream, _peer) = MockBleStream::pair(local.clone(), addr.clone(), 2048);
|
||||
Ok(stream)
|
||||
});
|
||||
|
||||
let stream = io.connect(&test_addr(2), 0x0085).await.unwrap();
|
||||
assert_eq!(stream.remote_addr(), &test_addr(2));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mock_io_connect_no_handler() {
|
||||
let io = MockBleIo::new("hci0", test_addr(1));
|
||||
let result = io.connect(&test_addr(2), 0x0085).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mock_io_scan() {
|
||||
let io = MockBleIo::new("hci0", test_addr(1));
|
||||
let mut scanner = io.start_scanning().await.unwrap();
|
||||
|
||||
io.inject_scan_result(test_addr(2)).await;
|
||||
io.inject_scan_result(test_addr(3)).await;
|
||||
|
||||
assert_eq!(scanner.next().await, Some(test_addr(2)));
|
||||
assert_eq!(scanner.next().await, Some(test_addr(3)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mock_io_local_addr() {
|
||||
let io = MockBleIo::new("hci0", test_addr(1));
|
||||
assert_eq!(io.local_addr().unwrap(), test_addr(1));
|
||||
assert_eq!(io.adapter_name(), "hci0");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mock_io_advertising_noop() {
|
||||
let io = MockBleIo::new("hci0", test_addr(1));
|
||||
io.start_advertising().await.unwrap();
|
||||
io.stop_advertising().await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mock_io_listen_twice_fails() {
|
||||
let io = MockBleIo::new("hci0", test_addr(1));
|
||||
let _acceptor = io.listen(0x0085).await.unwrap();
|
||||
assert!(io.listen(0x0085).await.is_err());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,294 @@
|
||||
//! BLE connection pool with priority eviction.
|
||||
//!
|
||||
//! BLE hardware limits concurrent connections (typically 4-10). The pool
|
||||
//! enforces a configurable maximum and prioritizes static (configured)
|
||||
//! peers over dynamically discovered ones.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use crate::transport::{TransportAddr, TransportError};
|
||||
|
||||
use super::addr::BleAddr;
|
||||
|
||||
/// A single BLE connection in the pool.
|
||||
pub struct BleConnection<S> {
|
||||
/// The L2CAP stream for this connection.
|
||||
pub stream: S,
|
||||
/// Background receive task handle.
|
||||
pub recv_task: Option<JoinHandle<()>>,
|
||||
/// Negotiated L2CAP send MTU.
|
||||
pub send_mtu: u16,
|
||||
/// Negotiated L2CAP receive MTU.
|
||||
pub recv_mtu: u16,
|
||||
/// When the connection was established.
|
||||
pub established_at: tokio::time::Instant,
|
||||
/// Whether this is a static (configured) peer.
|
||||
pub is_static: bool,
|
||||
/// Parsed remote address.
|
||||
pub addr: BleAddr,
|
||||
}
|
||||
|
||||
impl<S> BleConnection<S> {
|
||||
/// Effective MTU for this connection: min(send, recv).
|
||||
pub fn effective_mtu(&self) -> u16 {
|
||||
self.send_mtu.min(self.recv_mtu)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Drop for BleConnection<S> {
|
||||
fn drop(&mut self) {
|
||||
if let Some(task) = self.recv_task.take() {
|
||||
task.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Connection pool managing BLE connections with priority eviction.
|
||||
pub struct ConnectionPool<S> {
|
||||
connections: HashMap<TransportAddr, BleConnection<S>>,
|
||||
max_connections: usize,
|
||||
}
|
||||
|
||||
impl<S> ConnectionPool<S> {
|
||||
/// Create a new pool with the given maximum capacity.
|
||||
pub fn new(max_connections: usize) -> Self {
|
||||
Self {
|
||||
connections: HashMap::new(),
|
||||
max_connections,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the number of active connections.
|
||||
pub fn len(&self) -> usize {
|
||||
self.connections.len()
|
||||
}
|
||||
|
||||
/// Check if the pool is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.connections.is_empty()
|
||||
}
|
||||
|
||||
/// Check if the pool is at capacity.
|
||||
pub fn is_full(&self) -> bool {
|
||||
self.connections.len() >= self.max_connections
|
||||
}
|
||||
|
||||
/// Get the maximum pool capacity.
|
||||
pub fn max_connections(&self) -> usize {
|
||||
self.max_connections
|
||||
}
|
||||
|
||||
/// Look up a connection by transport address.
|
||||
pub fn get(&self, addr: &TransportAddr) -> Option<&BleConnection<S>> {
|
||||
self.connections.get(addr)
|
||||
}
|
||||
|
||||
/// Look up a mutable connection by transport address.
|
||||
pub fn get_mut(&mut self, addr: &TransportAddr) -> Option<&mut BleConnection<S>> {
|
||||
self.connections.get_mut(addr)
|
||||
}
|
||||
|
||||
/// Check if a connection exists for the given address.
|
||||
pub fn contains(&self, addr: &TransportAddr) -> bool {
|
||||
self.connections.contains_key(addr)
|
||||
}
|
||||
|
||||
/// Try to insert a connection, evicting if necessary.
|
||||
///
|
||||
/// Returns `Ok(evicted_addr)` on success (with optional evicted peer),
|
||||
/// or `Err` if the pool is full and the new connection cannot evict anyone.
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
addr: TransportAddr,
|
||||
conn: BleConnection<S>,
|
||||
) -> Result<Option<TransportAddr>, TransportError> {
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
// Already connected — replace
|
||||
if let Entry::Occupied(mut e) = self.connections.entry(addr.clone()) {
|
||||
e.insert(conn);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Room available
|
||||
if !self.is_full() {
|
||||
self.connections.insert(addr, conn);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Pool full — try eviction
|
||||
let evicted = self.find_eviction_candidate(conn.is_static)?;
|
||||
self.connections.remove(&evicted);
|
||||
self.connections.insert(addr, conn);
|
||||
Ok(Some(evicted))
|
||||
}
|
||||
|
||||
/// Remove a connection by address.
|
||||
pub fn remove(&mut self, addr: &TransportAddr) -> Option<BleConnection<S>> {
|
||||
self.connections.remove(addr)
|
||||
}
|
||||
|
||||
/// Get all connection addresses.
|
||||
pub fn addrs(&self) -> Vec<TransportAddr> {
|
||||
self.connections.keys().cloned().collect()
|
||||
}
|
||||
|
||||
/// Find the best eviction candidate.
|
||||
///
|
||||
/// Static peers requesting a slot can evict the oldest non-static peer.
|
||||
/// Non-static peers cannot evict anyone if all slots are static.
|
||||
fn find_eviction_candidate(
|
||||
&self,
|
||||
new_is_static: bool,
|
||||
) -> Result<TransportAddr, TransportError> {
|
||||
if new_is_static {
|
||||
// Static peer can evict oldest non-static
|
||||
self.connections
|
||||
.iter()
|
||||
.filter(|(_, c)| !c.is_static)
|
||||
.min_by_key(|(_, c)| c.established_at)
|
||||
.map(|(addr, _)| addr.clone())
|
||||
.ok_or_else(|| {
|
||||
TransportError::NotSupported(
|
||||
"BLE pool full: all connections are static".into(),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
// Non-static peer evicts oldest non-static
|
||||
self.connections
|
||||
.iter()
|
||||
.filter(|(_, c)| !c.is_static)
|
||||
.min_by_key(|(_, c)| c.established_at)
|
||||
.map(|(addr, _)| addr.clone())
|
||||
.ok_or_else(|| {
|
||||
TransportError::NotSupported(
|
||||
"BLE pool full: all connections are static".into(),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn test_addr(n: u8) -> TransportAddr {
|
||||
TransportAddr::from_string(&format!("hci0/AA:BB:CC:DD:EE:{n:02X}"))
|
||||
}
|
||||
|
||||
fn test_ble_addr(n: u8) -> BleAddr {
|
||||
BleAddr {
|
||||
adapter: "hci0".to_string(),
|
||||
device: [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, n],
|
||||
}
|
||||
}
|
||||
|
||||
fn test_conn(n: u8, is_static: bool) -> BleConnection<()> {
|
||||
BleConnection {
|
||||
stream: (),
|
||||
recv_task: None,
|
||||
send_mtu: 2048,
|
||||
recv_mtu: 2048,
|
||||
established_at: tokio::time::Instant::now(),
|
||||
is_static,
|
||||
addr: test_ble_addr(n),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pool_basic_insert() {
|
||||
let mut pool: ConnectionPool<()> = ConnectionPool::new(7);
|
||||
assert!(pool.is_empty());
|
||||
|
||||
pool.insert(test_addr(1), test_conn(1, false)).unwrap();
|
||||
assert_eq!(pool.len(), 1);
|
||||
assert!(!pool.is_empty());
|
||||
assert!(pool.contains(&test_addr(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pool_remove() {
|
||||
let mut pool: ConnectionPool<()> = ConnectionPool::new(7);
|
||||
pool.insert(test_addr(1), test_conn(1, false)).unwrap();
|
||||
assert!(pool.remove(&test_addr(1)).is_some());
|
||||
assert!(pool.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pool_full_eviction() {
|
||||
let mut pool: ConnectionPool<()> = ConnectionPool::new(3);
|
||||
pool.insert(test_addr(1), test_conn(1, false)).unwrap();
|
||||
pool.insert(test_addr(2), test_conn(2, false)).unwrap();
|
||||
pool.insert(test_addr(3), test_conn(3, false)).unwrap();
|
||||
assert!(pool.is_full());
|
||||
|
||||
// Inserting a 4th should evict the oldest non-static
|
||||
let result = pool.insert(test_addr(4), test_conn(4, false));
|
||||
assert!(result.is_ok());
|
||||
assert!(result.unwrap().is_some()); // something was evicted
|
||||
assert_eq!(pool.len(), 3);
|
||||
assert!(pool.contains(&test_addr(4)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pool_static_evicts_nonstatic() {
|
||||
let mut pool: ConnectionPool<()> = ConnectionPool::new(2);
|
||||
pool.insert(test_addr(1), test_conn(1, false)).unwrap();
|
||||
pool.insert(test_addr(2), test_conn(2, false)).unwrap();
|
||||
|
||||
// Static peer should evict a non-static
|
||||
let result = pool.insert(test_addr(3), test_conn(3, true));
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(pool.len(), 2);
|
||||
assert!(pool.contains(&test_addr(3)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pool_all_static_rejects() {
|
||||
let mut pool: ConnectionPool<()> = ConnectionPool::new(2);
|
||||
pool.insert(test_addr(1), test_conn(1, true)).unwrap();
|
||||
pool.insert(test_addr(2), test_conn(2, true)).unwrap();
|
||||
|
||||
// Non-static peer cannot evict static peers
|
||||
let result = pool.insert(test_addr(3), test_conn(3, false));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pool_replace_existing() {
|
||||
let mut pool: ConnectionPool<()> = ConnectionPool::new(2);
|
||||
pool.insert(test_addr(1), test_conn(1, false)).unwrap();
|
||||
|
||||
// Re-inserting same address should replace, not grow
|
||||
let result = pool.insert(test_addr(1), test_conn(1, true));
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(pool.len(), 1);
|
||||
assert!(pool.get(&test_addr(1)).unwrap().is_static);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pool_effective_mtu() {
|
||||
let mut conn = test_conn(1, false);
|
||||
conn.send_mtu = 1024;
|
||||
conn.recv_mtu = 2048;
|
||||
assert_eq!(conn.effective_mtu(), 1024);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pool_addrs() {
|
||||
let mut pool: ConnectionPool<()> = ConnectionPool::new(7);
|
||||
pool.insert(test_addr(1), test_conn(1, false)).unwrap();
|
||||
pool.insert(test_addr(2), test_conn(2, false)).unwrap();
|
||||
|
||||
let mut addrs = pool.addrs();
|
||||
addrs.sort_by(|a, b| a.as_str().cmp(&b.as_str()));
|
||||
assert_eq!(addrs.len(), 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
//! BLE transport statistics.
|
||||
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
/// Statistics for a BLE transport instance.
|
||||
///
|
||||
/// Uses atomic counters for lock-free updates from per-connection
|
||||
/// receive loops and the send path concurrently.
|
||||
pub struct BleStats {
|
||||
pub packets_sent: AtomicU64,
|
||||
pub bytes_sent: AtomicU64,
|
||||
pub packets_recv: AtomicU64,
|
||||
pub bytes_recv: AtomicU64,
|
||||
pub send_errors: AtomicU64,
|
||||
pub recv_errors: AtomicU64,
|
||||
pub mtu_exceeded: AtomicU64,
|
||||
pub connections_established: AtomicU64,
|
||||
pub connections_accepted: AtomicU64,
|
||||
pub connections_rejected: AtomicU64,
|
||||
pub connect_timeouts: AtomicU64,
|
||||
pub pool_evictions: AtomicU64,
|
||||
pub advertisements_sent: AtomicU64,
|
||||
pub scan_results: AtomicU64,
|
||||
}
|
||||
|
||||
impl BleStats {
|
||||
/// Create a new stats instance with all counters at zero.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
packets_sent: AtomicU64::new(0),
|
||||
bytes_sent: AtomicU64::new(0),
|
||||
packets_recv: AtomicU64::new(0),
|
||||
bytes_recv: AtomicU64::new(0),
|
||||
send_errors: AtomicU64::new(0),
|
||||
recv_errors: AtomicU64::new(0),
|
||||
mtu_exceeded: AtomicU64::new(0),
|
||||
connections_established: AtomicU64::new(0),
|
||||
connections_accepted: AtomicU64::new(0),
|
||||
connections_rejected: AtomicU64::new(0),
|
||||
connect_timeouts: AtomicU64::new(0),
|
||||
pool_evictions: AtomicU64::new(0),
|
||||
advertisements_sent: AtomicU64::new(0),
|
||||
scan_results: AtomicU64::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Record a successful send.
|
||||
pub fn record_send(&self, bytes: usize) {
|
||||
self.packets_sent.fetch_add(1, Ordering::Relaxed);
|
||||
self.bytes_sent.fetch_add(bytes as u64, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Record a successful receive.
|
||||
pub fn record_recv(&self, bytes: usize) {
|
||||
self.packets_recv.fetch_add(1, Ordering::Relaxed);
|
||||
self.bytes_recv.fetch_add(bytes as u64, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Record a send error.
|
||||
pub fn record_send_error(&self) {
|
||||
self.send_errors.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Record a receive error.
|
||||
pub fn record_recv_error(&self) {
|
||||
self.recv_errors.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Record an MTU exceeded rejection.
|
||||
pub fn record_mtu_exceeded(&self) {
|
||||
self.mtu_exceeded.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Record a successful outbound connection.
|
||||
pub fn record_connection_established(&self) {
|
||||
self.connections_established.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Record a successful inbound connection.
|
||||
pub fn record_connection_accepted(&self) {
|
||||
self.connections_accepted.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Record a rejected inbound connection (pool full).
|
||||
pub fn record_connection_rejected(&self) {
|
||||
self.connections_rejected.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Record a connect timeout.
|
||||
pub fn record_connect_timeout(&self) {
|
||||
self.connect_timeouts.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Record a pool eviction (non-static peer displaced).
|
||||
pub fn record_pool_eviction(&self) {
|
||||
self.pool_evictions.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Record an advertisement broadcast.
|
||||
pub fn record_advertisement(&self) {
|
||||
self.advertisements_sent.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Record a scan result received.
|
||||
pub fn record_scan_result(&self) {
|
||||
self.scan_results.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Take a snapshot of all counters.
|
||||
pub fn snapshot(&self) -> BleStatsSnapshot {
|
||||
BleStatsSnapshot {
|
||||
packets_sent: self.packets_sent.load(Ordering::Relaxed),
|
||||
bytes_sent: self.bytes_sent.load(Ordering::Relaxed),
|
||||
packets_recv: self.packets_recv.load(Ordering::Relaxed),
|
||||
bytes_recv: self.bytes_recv.load(Ordering::Relaxed),
|
||||
send_errors: self.send_errors.load(Ordering::Relaxed),
|
||||
recv_errors: self.recv_errors.load(Ordering::Relaxed),
|
||||
mtu_exceeded: self.mtu_exceeded.load(Ordering::Relaxed),
|
||||
connections_established: self.connections_established.load(Ordering::Relaxed),
|
||||
connections_accepted: self.connections_accepted.load(Ordering::Relaxed),
|
||||
connections_rejected: self.connections_rejected.load(Ordering::Relaxed),
|
||||
connect_timeouts: self.connect_timeouts.load(Ordering::Relaxed),
|
||||
pool_evictions: self.pool_evictions.load(Ordering::Relaxed),
|
||||
advertisements_sent: self.advertisements_sent.load(Ordering::Relaxed),
|
||||
scan_results: self.scan_results.load(Ordering::Relaxed),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BleStats {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Point-in-time snapshot of BLE stats (non-atomic, copyable).
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct BleStatsSnapshot {
|
||||
pub packets_sent: u64,
|
||||
pub bytes_sent: u64,
|
||||
pub packets_recv: u64,
|
||||
pub bytes_recv: u64,
|
||||
pub send_errors: u64,
|
||||
pub recv_errors: u64,
|
||||
pub mtu_exceeded: u64,
|
||||
pub connections_established: u64,
|
||||
pub connections_accepted: u64,
|
||||
pub connections_rejected: u64,
|
||||
pub connect_timeouts: u64,
|
||||
pub pool_evictions: u64,
|
||||
pub advertisements_sent: u64,
|
||||
pub scan_results: u64,
|
||||
}
|
||||
@@ -11,6 +11,8 @@ pub mod tor;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod ethernet;
|
||||
|
||||
pub mod ble;
|
||||
|
||||
use secp256k1::XOnlyPublicKey;
|
||||
use udp::UdpTransport;
|
||||
use tcp::TcpTransport;
|
||||
@@ -18,6 +20,8 @@ use tor::control::TorMonitoringInfo;
|
||||
use tor::TorTransport;
|
||||
#[cfg(target_os = "linux")]
|
||||
use ethernet::EthernetTransport;
|
||||
#[cfg(target_os = "linux")]
|
||||
use ble::DefaultBleTransport;
|
||||
use std::fmt;
|
||||
use std::net::SocketAddr;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
@@ -235,6 +239,13 @@ impl TransportType {
|
||||
reliable: true, // typically uses framing with checksums
|
||||
};
|
||||
|
||||
/// BLE L2CAP CoC transport.
|
||||
pub const BLE: TransportType = TransportType {
|
||||
name: "ble",
|
||||
connection_oriented: true,
|
||||
reliable: true, // L2CAP SeqPacket guarantees delivery
|
||||
};
|
||||
|
||||
/// Check if the transport is connectionless.
|
||||
pub fn is_connectionless(&self) -> bool {
|
||||
!self.connection_oriented
|
||||
@@ -847,6 +858,9 @@ pub enum TransportHandle {
|
||||
Tcp(TcpTransport),
|
||||
/// Tor transport (via SOCKS5).
|
||||
Tor(TorTransport),
|
||||
/// BLE L2CAP transport.
|
||||
#[cfg(target_os = "linux")]
|
||||
Ble(DefaultBleTransport),
|
||||
}
|
||||
|
||||
impl TransportHandle {
|
||||
@@ -858,6 +872,8 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(t) => t.start_async().await,
|
||||
TransportHandle::Tcp(t) => t.start_async().await,
|
||||
TransportHandle::Tor(t) => t.start_async().await,
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.start_async().await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -869,6 +885,8 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(t) => t.stop_async().await,
|
||||
TransportHandle::Tcp(t) => t.stop_async().await,
|
||||
TransportHandle::Tor(t) => t.stop_async().await,
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.stop_async().await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -880,6 +898,8 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(t) => t.send_async(addr, data).await,
|
||||
TransportHandle::Tcp(t) => t.send_async(addr, data).await,
|
||||
TransportHandle::Tor(t) => t.send_async(addr, data).await,
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.send_async(addr, data).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -891,6 +911,8 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(t) => t.transport_id(),
|
||||
TransportHandle::Tcp(t) => t.transport_id(),
|
||||
TransportHandle::Tor(t) => t.transport_id(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.transport_id(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -902,6 +924,8 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(t) => t.name(),
|
||||
TransportHandle::Tcp(t) => t.name(),
|
||||
TransportHandle::Tor(t) => t.name(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.name(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -913,6 +937,8 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(t) => t.transport_type(),
|
||||
TransportHandle::Tcp(t) => t.transport_type(),
|
||||
TransportHandle::Tor(t) => t.transport_type(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.transport_type(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -924,6 +950,8 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(t) => t.state(),
|
||||
TransportHandle::Tcp(t) => t.state(),
|
||||
TransportHandle::Tor(t) => t.state(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.state(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -935,6 +963,8 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(t) => t.mtu(),
|
||||
TransportHandle::Tcp(t) => t.mtu(),
|
||||
TransportHandle::Tor(t) => t.mtu(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.mtu(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -949,6 +979,8 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(t) => t.link_mtu(addr),
|
||||
TransportHandle::Tcp(t) => t.link_mtu(addr),
|
||||
TransportHandle::Tor(t) => t.link_mtu(addr),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.link_mtu(addr),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -960,6 +992,8 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(_) => None,
|
||||
TransportHandle::Tcp(t) => t.local_addr(),
|
||||
TransportHandle::Tor(_) => None,
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -971,6 +1005,8 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(t) => Some(t.interface_name()),
|
||||
TransportHandle::Tcp(_) => None,
|
||||
TransportHandle::Tor(_) => None,
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1006,6 +1042,8 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(t) => t.discover(),
|
||||
TransportHandle::Tcp(t) => t.discover(),
|
||||
TransportHandle::Tor(t) => t.discover(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.discover(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1017,6 +1055,8 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(t) => t.auto_connect(),
|
||||
TransportHandle::Tcp(t) => t.auto_connect(),
|
||||
TransportHandle::Tor(t) => t.auto_connect(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.auto_connect(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1028,6 +1068,8 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(t) => t.accept_connections(),
|
||||
TransportHandle::Tcp(t) => t.accept_connections(),
|
||||
TransportHandle::Tor(t) => t.accept_connections(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.accept_connections(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1045,6 +1087,8 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(_) => Ok(()), // connectionless
|
||||
TransportHandle::Tcp(t) => t.connect_async(addr).await,
|
||||
TransportHandle::Tor(t) => t.connect_async(addr).await,
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.connect_async(addr).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1060,6 +1104,8 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(_) => ConnectionState::Connected,
|
||||
TransportHandle::Tcp(t) => t.connection_state_sync(addr),
|
||||
TransportHandle::Tor(t) => t.connection_state_sync(addr),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.connection_state_sync(addr),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1074,6 +1120,8 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(t) => t.close_connection(addr),
|
||||
TransportHandle::Tcp(t) => t.close_connection_async(addr).await,
|
||||
TransportHandle::Tor(t) => t.close_connection_async(addr).await,
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.close_connection_async(addr).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1094,6 +1142,8 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(_) => TransportCongestion::default(),
|
||||
TransportHandle::Tcp(_) => TransportCongestion::default(),
|
||||
TransportHandle::Tor(_) => TransportCongestion::default(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(_) => TransportCongestion::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1127,6 +1177,10 @@ impl TransportHandle {
|
||||
TransportHandle::Tor(t) => {
|
||||
serde_json::to_value(t.stats().snapshot()).unwrap_or_default()
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => {
|
||||
serde_json::to_value(t.stats().snapshot()).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user