diff --git a/build.rs b/build.rs index 06ab0d5f..0b61861c 100644 --- a/build.rs +++ b/build.rs @@ -43,7 +43,22 @@ fn main() { println!("cargo:rustc-check-cfg=cfg(bluer_available)"); let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); let target_env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default(); - if target_os == "linux" && target_env != "musl" { + let bluer_available = target_os == "linux" && target_env != "musl"; + if bluer_available { println!("cargo:rustc-cfg=bluer_available"); } + + // Whether the BLE transport is compiled at all. + // + // This is the set of platforms that have a concrete `BleIo` backend, not + // the set that could plausibly have one. A platform listed here with no + // backend behind it does not get "BLE, degraded" — it gets an in-memory + // transport that starts, reports itself up and never peers, with no error + // anywhere. Add a platform here only in the same change that adds its + // backend; `transport::ble` carries a compile-time tripwire that refuses + // a build where the two disagree. + println!("cargo:rustc-check-cfg=cfg(ble_available)"); + if bluer_available || target_os == "android" { + println!("cargo:rustc-cfg=ble_available"); + } } diff --git a/src/node/lifecycle/mod.rs b/src/node/lifecycle/mod.rs index 4e5f7c20..1e40b272 100644 --- a/src/node/lifecycle/mod.rs +++ b/src/node/lifecycle/mod.rs @@ -2474,7 +2474,7 @@ impl Node { } } } else if addr.transport == "ble" { - #[cfg(bluer_available)] + #[cfg(ble_available)] { match self.resolve_ble_addr(&addr.addr) { Ok(result) => result, @@ -2489,7 +2489,7 @@ impl Node { } } } - #[cfg(not(bluer_available))] + #[cfg(not(ble_available))] { debug!(transport = %addr.transport, "BLE transport not available on this build"); continue; diff --git a/src/node/mod.rs b/src/node/mod.rs index ab0c30f9..ec93cb04 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -541,6 +541,17 @@ pub struct Node { /// TUN interface name (for cleanup). tun_name: Option, + /// Slot the embedder installs its BLE radio into, armed by + /// [`Self::enable_app_owned_ble_radio`]. `None` unless armed. + /// + /// Gated on the BLE transport existing *and* on its backend being the + /// embedder-supplied one — the same condition + /// `transport::ble::io_android` itself is compiled under, so the seam is + /// absent on platforms whose radio is opened in process, and present in a + /// test build so its contract is covered on an ordinary runner. + #[cfg(all(ble_available, any(target_os = "android", test)))] + ble_radio: Option>, + // === Index-Based Session Dispatch === /// Allocator for session indices. index_allocator: IndexAllocator, @@ -833,6 +844,8 @@ impl Node { )), tun_state, tun_name: None, + #[cfg(all(ble_available, any(target_os = "android", test)))] + ble_radio: None, index_allocator: IndexAllocator::new(), peers_by_index: HashMap::new(), pending_outbound: HashMap::new(), @@ -996,6 +1009,8 @@ impl Node { )), tun_state, tun_name: None, + #[cfg(all(ble_available, any(target_os = "android", test)))] + ble_radio: None, index_allocator: IndexAllocator::new(), peers_by_index: HashMap::new(), pending_outbound: HashMap::new(), @@ -1189,6 +1204,33 @@ impl Node { } } + // Create BLE transport instances over an embedder-supplied radio. + // Built whether or not a radio is installed yet: the backend resolves + // the slot per operation, so one that arrives later is adopted in + // place rather than needing the node rebuilt around it. + #[cfg(all(target_os = "android", not(bluer_available), not(test)))] + if let Some(slot) = self.ble_radio.clone() { + let ble_instances: Vec<_> = self + .config() + .transports + .ble + .iter() + .map(|(name, config)| (name.map(|s| s.to_string()), config.clone())) + .collect(); + for (name, ble_config) in ble_instances { + let transport_id = self.allocate_transport_id(); + let mut ble = crate::transport::ble::BleTransport::new( + transport_id, + name, + ble_config, + crate::transport::ble::io_android::AndroidIo::new(Arc::clone(&slot)), + packet_tx.clone(), + ); + ble.set_local_pubkey(self.identity().pubkey().serialize()); + transports.push(TransportHandle::Ble(ble)); + } + } + transports } @@ -1255,7 +1297,7 @@ impl Node { /// 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(bluer_available)] + #[cfg(ble_available)] 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(|| { @@ -3339,6 +3381,56 @@ impl Node { udp_fd_rx } + /// Set up an **app-owned BLE radio**: the embedder supplies the radio the + /// BLE transport drives, because on this platform there is no + /// Rust-reachable one to open. Call this after [`Node::new`] and + /// **before** [`Self::start`] — the transport is built during `start`, and + /// only a node armed by then has a slot to build it over. + /// + /// Returns the slot. Installing, replacing and clearing a radio through it + /// is safe at any time, from any thread, including long after the node is + /// running: + /// + /// ```no_run + /// # async fn f(node: &mut fips::Node, radio: std::sync::Arc) + /// # -> Result<(), Box> { + /// let slot = node.enable_app_owned_ble_radio(); // after new(), before start() + /// node.start().await?; + /// // ...whenever the embedder's radio service comes up, and again each + /// // time it restarts: + /// slot.install(fips::transport::ble::io_android::AndroidBleBridge::new(radio)); + /// # Ok(()) + /// # } + /// ``` + /// + /// The lateness is the point rather than a convenience. The radio belongs + /// to a service whose lifetime is not the node's: the user can turn it on + /// after the mesh is already running, and off and on again, and each start + /// typically produces a fresh radio. A node that had to be built around an + /// existing radio would make that mean "tear the node down and rebuild + /// it", dropping every peer, session and route for as long as + /// re-handshaking takes. So the transport is built and started whether or + /// not a radio is installed, and resolves the slot per operation: it + /// listens and scans against whichever radio is there, dials fail while + /// there is none, and everything recovers on its own when one appears. + /// Streams already open keep the radio they were opened on rather than + /// migrating. + /// + /// Deliberately narrow, and shaped like the [`Self::enable_app_owned_tun`] + /// seam it sits beside: one call, no callbacks, and no lifecycle contract + /// beyond the slot outliving the node. Arming twice returns the same slot, + /// so a second call cannot orphan a radio installed through the first. The + /// seam does not exist on platforms whose BLE backend is opened in + /// process, since there is nothing there for an embedder to supply. + #[cfg(all(ble_available, any(target_os = "android", test)))] + pub fn enable_app_owned_ble_radio( + &mut self, + ) -> Arc { + Arc::clone(self.ble_radio.get_or_insert_with(|| { + Arc::new(crate::transport::ble::io_android::BleRadioSlot::new()) + })) + } + /// Address the built-in `.fips` DNS responder is listening on, or `None` /// when it is not running (`dns.enabled = false`, the bind failed, or the /// node is stopped). diff --git a/src/node/tests/mod.rs b/src/node/tests/mod.rs index e33d946e..dd56ed0d 100644 --- a/src/node/tests/mod.rs +++ b/src/node/tests/mod.rs @@ -6,7 +6,7 @@ use crate::utils::index::SessionIndex; use std::time::Duration; mod acl; -#[cfg(target_os = "linux")] +#[cfg(ble_available)] mod ble; mod bloom; mod bloom_poison; diff --git a/src/node/tests/unit.rs b/src/node/tests/unit.rs index a6157729..12b0ff7b 100644 --- a/src/node/tests/unit.rs +++ b/src/node/tests/unit.rs @@ -3544,6 +3544,115 @@ fn app_owned_udp_fd_seam_second_arm_replaces_the_first() { ); } +/// The app-owned BLE radio seam. The slot is live from the moment it is armed +/// — before `start()`, which is when the transport that reads it gets built — +/// and installing a radio through it is a slot operation, not a node one. +#[cfg(all(ble_available, any(target_os = "android", test)))] +#[test] +fn app_owned_ble_radio_seam_hands_out_a_live_slot_before_start() { + use crate::transport::ble::io_android::{AndroidBleBridge, BleRadioSlot}; + + let mut node = make_node(); + let slot: std::sync::Arc = node.enable_app_owned_ble_radio(); + + assert!( + !slot.is_installed(), + "arming supplies the slot, not a radio to put in it", + ); + + slot.install(AndroidBleBridge::new(std::sync::Arc::new( + test_radio::TestRadio, + ))); + assert!(slot.is_installed(), "the embedder installs whenever it can"); + + slot.clear(); + assert!(!slot.is_installed(), "and can take it away again"); +} + +/// Arming twice returns the same slot, so a second call cannot orphan a radio +/// installed through the first. This is where the seam deliberately differs +/// from `enable_app_owned_udp_fd`, whose last arming wins: a channel can be +/// replaced because nothing was delivered on it yet, while a slot may already +/// be holding the embedder's live radio. +#[cfg(all(ble_available, any(target_os = "android", test)))] +#[test] +fn app_owned_ble_radio_seam_second_arm_returns_the_same_slot() { + use crate::transport::ble::io_android::AndroidBleBridge; + + let mut node = make_node(); + let first = node.enable_app_owned_ble_radio(); + first.install(AndroidBleBridge::new(std::sync::Arc::new( + test_radio::TestRadio, + ))); + + let second = node.enable_app_owned_ble_radio(); + + assert!( + std::sync::Arc::ptr_eq(&first, &second), + "re-arming must not hand back a different slot", + ); + assert!( + second.is_installed(), + "the radio installed through the first handle is still there", + ); +} + +/// The slot is per-node state, which is the whole reason it is not a process +/// global: two nodes in one process each drive their own radio. +#[cfg(all(ble_available, any(target_os = "android", test)))] +#[test] +fn app_owned_ble_radio_slots_are_per_node() { + use crate::transport::ble::io_android::AndroidBleBridge; + + let mut node_a = make_node(); + let mut node_b = make_node(); + let slot_a = node_a.enable_app_owned_ble_radio(); + let slot_b = node_b.enable_app_owned_ble_radio(); + + slot_a.install(AndroidBleBridge::new(std::sync::Arc::new( + test_radio::TestRadio, + ))); + + assert!(slot_a.is_installed()); + assert!( + !slot_b.is_installed(), + "node B's radio is node B's — no shared or global slot", + ); +} + +/// A node that never armed the seam has no slot to hand the transport, which +/// is how a build with the embedder-supplied backend distinguishes "no radio +/// yet" from "this embedder does not supply one at all". +#[cfg(all(ble_available, any(target_os = "android", test)))] +#[test] +fn app_owned_ble_radio_seam_is_absent_until_armed() { + let node = make_node(); + assert!(node.ble_radio.is_none()); +} + +#[cfg(all(ble_available, any(target_os = "android", test)))] +mod test_radio { + use crate::transport::ble::addr::BleAddr; + use crate::transport::ble::io_android::AndroidRadio; + + /// A radio that does nothing. These tests are about the seam handing one + /// over, not about what it then does — that is covered where the backend + /// lives. + pub(super) struct TestRadio; + + impl AndroidRadio for TestRadio { + fn listen(&self) -> u16 { + 0 + } + fn connect(&self, _connect_id: i64, _addr: &BleAddr, _psm: u16) {} + fn start_advertising(&self, _psm: u16) {} + fn stop_advertising(&self) {} + fn start_scanning(&self) {} + fn stop_scanning(&self) {} + fn close_channel(&self, _ch_id: i64) {} + } +} + /// The embedder-facing DNS contract, end to end. /// /// An embedder that owns the TUN fd (Android `VpnService`) has no system DNS diff --git a/src/transport/ble/mod.rs b/src/transport/ble/mod.rs index fb20b58b..69842010 100644 --- a/src/transport/ble/mod.rs +++ b/src/transport/ble/mod.rs @@ -16,10 +16,13 @@ //! //! ## Architecture //! -//! Transport logic (pool, neighbor, lifecycle) is separated from the -//! BlueZ/bluer stack via the `BleIo` trait. `BluerIo` provides the real -//! implementation (behind `cfg(bluer_available)`); `MockBleIo` provides -//! an in-memory test double for CI without hardware. +//! Transport logic (pool, neighbor, lifecycle) is separated from any one +//! Bluetooth stack via the `BleIo` trait. `BluerIo` drives BlueZ (behind +//! `cfg(bluer_available)`), [`io_android::AndroidIo`] drives a radio the +//! embedder supplies, and `MockBleIo` is an in-memory double for tests +//! without hardware. Which one `DefaultBleTransport` resolves to is decided +//! by the cascade below, and the whole module is compiled only on platforms +//! that have one of them — see `ble_available` in `build.rs`. //! //! ## Connection Pool //! @@ -80,16 +83,41 @@ use tracing::{debug, info, trace, warn}; /// dialled at when it advertises nothing. pub const DEFAULT_PSM: u16 = 0x0085; -/// Concrete BLE transport type for use in TransportHandle. +/// Concrete BLE transport type for use in `TransportHandle`. /// -/// Production builds on glibc-linux use `BluerIo` (real BlueZ stack). -/// Test builds, musl-linux, and non-Linux platforms use `MockBleIo`. +/// Three arms, in priority order: an in-process BlueZ stack where one exists, +/// otherwise a radio the embedder supplies, otherwise — and *only* in a test +/// build — the in-memory double. +/// +/// The mock arm is deliberately not written as "anything that is not BlueZ". +/// That phrasing is what makes widening the module gate dangerous: a platform +/// added to `ble_available` without a backend would silently land on an +/// in-memory transport that compiles, starts, reports [`TransportState::Up`] +/// and never peers, with nothing anywhere to say so. The tripwire below makes +/// that state unrepresentable instead. #[cfg(all(bluer_available, not(test)))] pub type DefaultBleTransport = BleTransport; -#[cfg(any(not(bluer_available), test))] +#[cfg(all(target_os = "android", not(bluer_available), not(test)))] +pub type DefaultBleTransport = BleTransport; + +#[cfg(test)] pub type DefaultBleTransport = BleTransport; +// The tripwire. This module is only compiled when `ble_available`, so +// reaching here means a platform declared it has BLE while having no concrete +// backend to provide it. It cannot fire today; it exists for whoever next +// widens `ble_available`, and it fails the build rather than shipping a +// transport that quietly never connects. +#[cfg(all(not(test), not(bluer_available), not(target_os = "android")))] +compile_error!( + "this target is `ble_available` but has no concrete `BleIo` backend. \ + Add its backend and an arm to the `DefaultBleTransport` cascade in \ + src/transport/ble/mod.rs, or drop the target from `ble_available` in \ + build.rs. Falling back to the in-memory mock in a non-test build would \ + produce a BLE transport that starts, reports itself up, and never peers." +); + // ============================================================================ // BLE Transport // ============================================================================ @@ -1708,6 +1736,29 @@ mod tests { assert!(p.position(&a(1)).is_none()); } + /// The mock backend is a *test* backend. Any target that compiles this + /// module must have a real one behind it, or a release build of it would + /// ship a BLE transport that starts, reports itself up, and never peers. + /// + /// The `compile_error!` above is what enforces that in a non-test build — + /// and by construction it cannot fire in a test build, which is exactly + /// the build everybody runs. This closes that gap: the `cfg!` values below + /// are evaluated for the *target*, not for the test profile, so this + /// asserts the same condition the tripwire does, from the one place a + /// developer will actually see it. + #[test] + fn a_target_that_compiles_this_module_has_a_real_backend() { + let has_concrete_backend = cfg!(bluer_available) || cfg!(target_os = "android"); + assert!( + has_concrete_backend, + "target {} is `ble_available` but has no concrete `BleIo` backend, \ + so a non-test build of it would select the in-memory mock. Add \ + its backend and an arm to the `DefaultBleTransport` cascade, or \ + drop it from `ble_available` in build.rs.", + std::env::consts::OS, + ); + } + /// Deterministic x-only pubkey for exchange tests. fn test_pubkey(seed: u8) -> [u8; 32] { let secp = Secp256k1::new(); diff --git a/src/transport/mod.rs b/src/transport/mod.rs index 4620f9f6..bb39a6d3 100644 --- a/src/transport/mod.rs +++ b/src/transport/mod.rs @@ -15,10 +15,10 @@ pub mod udp; #[cfg(any(target_os = "linux", target_os = "macos"))] pub mod ethernet; -#[cfg(target_os = "linux")] +#[cfg(ble_available)] pub mod ble; -#[cfg(target_os = "linux")] +#[cfg(ble_available)] use ble::DefaultBleTransport; #[cfg(any(target_os = "linux", target_os = "macos"))] use ethernet::EthernetTransport; @@ -673,7 +673,7 @@ pub enum TransportHandle { /// Nym mixnet transport (via SOCKS5). Nym(NymTransport), /// BLE L2CAP transport. - #[cfg(target_os = "linux")] + #[cfg(ble_available)] Ble(DefaultBleTransport), /// In-process loopback transport (test harness only). #[cfg(test)] @@ -690,7 +690,7 @@ impl TransportHandle { TransportHandle::Tcp(t) => t.start_async().await, TransportHandle::Tor(t) => t.start_async().await, TransportHandle::Nym(t) => t.start_async().await, - #[cfg(target_os = "linux")] + #[cfg(ble_available)] TransportHandle::Ble(t) => t.start_async().await, #[cfg(test)] TransportHandle::Loopback(t) => t.start_async().await, @@ -706,7 +706,7 @@ impl TransportHandle { TransportHandle::Tcp(t) => t.stop_async().await, TransportHandle::Tor(t) => t.stop_async().await, TransportHandle::Nym(t) => t.stop_async().await, - #[cfg(target_os = "linux")] + #[cfg(ble_available)] TransportHandle::Ble(t) => t.stop_async().await, #[cfg(test)] TransportHandle::Loopback(t) => t.stop_async().await, @@ -722,7 +722,7 @@ impl TransportHandle { TransportHandle::Tcp(t) => t.send_async(addr, data).await, TransportHandle::Tor(t) => t.send_async(addr, data).await, TransportHandle::Nym(t) => t.send_async(addr, data).await, - #[cfg(target_os = "linux")] + #[cfg(ble_available)] TransportHandle::Ble(t) => t.send_async(addr, data).await, #[cfg(test)] TransportHandle::Loopback(t) => t.send_async(addr, data).await, @@ -738,7 +738,7 @@ impl TransportHandle { TransportHandle::Tcp(t) => t.transport_id(), TransportHandle::Tor(t) => t.transport_id(), TransportHandle::Nym(t) => t.transport_id(), - #[cfg(target_os = "linux")] + #[cfg(ble_available)] TransportHandle::Ble(t) => t.transport_id(), #[cfg(test)] TransportHandle::Loopback(t) => t.transport_id(), @@ -754,7 +754,7 @@ impl TransportHandle { TransportHandle::Tcp(t) => t.name(), TransportHandle::Tor(t) => t.name(), TransportHandle::Nym(t) => t.name(), - #[cfg(target_os = "linux")] + #[cfg(ble_available)] TransportHandle::Ble(t) => t.name(), #[cfg(test)] TransportHandle::Loopback(_) => None, @@ -770,7 +770,7 @@ impl TransportHandle { TransportHandle::Tcp(t) => t.transport_type(), TransportHandle::Tor(t) => t.transport_type(), TransportHandle::Nym(t) => t.transport_type(), - #[cfg(target_os = "linux")] + #[cfg(ble_available)] TransportHandle::Ble(t) => t.transport_type(), #[cfg(test)] TransportHandle::Loopback(t) => t.transport_type(), @@ -786,7 +786,7 @@ impl TransportHandle { TransportHandle::Tcp(t) => t.state(), TransportHandle::Tor(t) => t.state(), TransportHandle::Nym(t) => t.state(), - #[cfg(target_os = "linux")] + #[cfg(ble_available)] TransportHandle::Ble(t) => t.state(), #[cfg(test)] TransportHandle::Loopback(t) => t.state(), @@ -802,7 +802,7 @@ impl TransportHandle { TransportHandle::Tcp(t) => t.mtu(), TransportHandle::Tor(t) => t.mtu(), TransportHandle::Nym(t) => t.mtu(), - #[cfg(target_os = "linux")] + #[cfg(ble_available)] TransportHandle::Ble(t) => t.mtu(), #[cfg(test)] TransportHandle::Loopback(t) => t.mtu(), @@ -821,7 +821,7 @@ impl TransportHandle { TransportHandle::Tcp(t) => t.link_mtu(addr), TransportHandle::Tor(t) => t.link_mtu(addr), TransportHandle::Nym(t) => t.link_mtu(addr), - #[cfg(target_os = "linux")] + #[cfg(ble_available)] TransportHandle::Ble(t) => t.link_mtu(addr), #[cfg(test)] TransportHandle::Loopback(t) => t.link_mtu(addr), @@ -837,7 +837,7 @@ impl TransportHandle { TransportHandle::Tcp(t) => t.local_addr(), TransportHandle::Tor(_) => None, TransportHandle::Nym(_) => None, - #[cfg(target_os = "linux")] + #[cfg(ble_available)] TransportHandle::Ble(_) => None, #[cfg(test)] TransportHandle::Loopback(_) => None, @@ -857,7 +857,7 @@ impl TransportHandle { TransportHandle::Tcp(_) => None, TransportHandle::Tor(_) => None, TransportHandle::Nym(_) => None, - #[cfg(target_os = "linux")] + #[cfg(ble_available)] TransportHandle::Ble(_) => None, #[cfg(test)] TransportHandle::Loopback(_) => None, @@ -873,7 +873,7 @@ impl TransportHandle { TransportHandle::Tcp(_) => None, TransportHandle::Tor(_) => None, TransportHandle::Nym(_) => None, - #[cfg(target_os = "linux")] + #[cfg(ble_available)] TransportHandle::Ble(_) => None, #[cfg(test)] TransportHandle::Loopback(_) => None, @@ -913,7 +913,7 @@ impl TransportHandle { TransportHandle::Tcp(t) => t.discover(), TransportHandle::Tor(t) => t.discover(), TransportHandle::Nym(t) => t.discover(), - #[cfg(target_os = "linux")] + #[cfg(ble_available)] TransportHandle::Ble(t) => t.discover(), #[cfg(test)] TransportHandle::Loopback(t) => t.discover(), @@ -929,7 +929,7 @@ impl TransportHandle { TransportHandle::Tcp(t) => t.auto_connect(), TransportHandle::Tor(t) => t.auto_connect(), TransportHandle::Nym(t) => t.auto_connect(), - #[cfg(target_os = "linux")] + #[cfg(ble_available)] TransportHandle::Ble(t) => t.auto_connect(), #[cfg(test)] TransportHandle::Loopback(t) => t.auto_connect(), @@ -945,7 +945,7 @@ impl TransportHandle { TransportHandle::Tcp(t) => t.accept_connections(), TransportHandle::Tor(t) => t.accept_connections(), TransportHandle::Nym(t) => t.accept_connections(), - #[cfg(target_os = "linux")] + #[cfg(ble_available)] TransportHandle::Ble(t) => t.accept_connections(), #[cfg(test)] TransportHandle::Loopback(t) => t.accept_connections(), @@ -967,7 +967,7 @@ impl TransportHandle { TransportHandle::Tcp(t) => t.connect_async(addr).await, TransportHandle::Tor(t) => t.connect_async(addr).await, TransportHandle::Nym(t) => t.connect_async(addr).await, - #[cfg(target_os = "linux")] + #[cfg(ble_available)] TransportHandle::Ble(t) => t.connect_async(addr).await, #[cfg(test)] TransportHandle::Loopback(_) => Ok(()), // connectionless @@ -987,7 +987,7 @@ impl TransportHandle { TransportHandle::Tcp(t) => t.connection_state_sync(addr), TransportHandle::Tor(t) => t.connection_state_sync(addr), TransportHandle::Nym(t) => t.connection_state_sync(addr), - #[cfg(target_os = "linux")] + #[cfg(ble_available)] TransportHandle::Ble(t) => t.connection_state_sync(addr), #[cfg(test)] TransportHandle::Loopback(_) => ConnectionState::Connected, @@ -1006,7 +1006,7 @@ impl TransportHandle { TransportHandle::Tcp(t) => t.close_connection_async(addr).await, TransportHandle::Tor(t) => t.close_connection_async(addr).await, TransportHandle::Nym(t) => t.close_connection_async(addr).await, - #[cfg(target_os = "linux")] + #[cfg(ble_available)] TransportHandle::Ble(t) => t.close_connection_async(addr).await, #[cfg(test)] TransportHandle::Loopback(_) => {} // connectionless no-op @@ -1031,7 +1031,7 @@ impl TransportHandle { TransportHandle::Tcp(_) => TransportCongestion::default(), TransportHandle::Tor(_) => TransportCongestion::default(), TransportHandle::Nym(_) => TransportCongestion::default(), - #[cfg(target_os = "linux")] + #[cfg(ble_available)] TransportHandle::Ble(_) => TransportCongestion::default(), #[cfg(test)] TransportHandle::Loopback(_) => TransportCongestion::default(), @@ -1059,7 +1059,7 @@ impl TransportHandle { TransportHandle::Nym(t) => { serde_json::to_value(t.stats().snapshot()).unwrap_or_default() } - #[cfg(target_os = "linux")] + #[cfg(ble_available)] TransportHandle::Ble(t) => { serde_json::to_value(t.stats().snapshot()).unwrap_or_default() }