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:
Johnathan Corgan
2026-03-25 04:21:46 +00:00
parent d3385b902a
commit 89352d3218
27 changed files with 5098 additions and 54 deletions
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+277
View File
@@ -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;
}
+2
View File
@@ -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")]
+185
View File
@@ -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);
}
}
+126
View File
@@ -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")
);
}
}
+774
View File
@@ -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
+294
View File
@@ -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);
}
}
+155
View File
@@ -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,
}
+54
View File
@@ -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()
}
}
}
}