mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-22 07:48:26 +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:
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -44,6 +44,9 @@ jobs:
|
||||
- name: Set SOURCE_DATE_EPOCH from git
|
||||
run: echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install system dependencies
|
||||
run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
@@ -92,6 +95,9 @@ jobs:
|
||||
- name: Set SOURCE_DATE_EPOCH from git
|
||||
run: echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install system dependencies
|
||||
run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
|
||||
11
CHANGELOG.md
11
CHANGELOG.md
@@ -16,6 +16,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
([#21](https://github.com/jmcorgan/fips/pull/21),
|
||||
[@dskvr](https://github.com/dskvr))
|
||||
|
||||
#### Bluetooth Transport
|
||||
|
||||
- Bluetooth Low Energy (BLE) L2CAP Connection-Oriented Channel transport
|
||||
with per-link MTU negotiation, behind the `ble` Cargo feature flag
|
||||
(default-on, Linux only, requires BlueZ)
|
||||
- BLE peer discovery via scan/probe with per-entry random jitter to
|
||||
prevent herd effects when multiple nodes see the same beacon
|
||||
- Periodic BLE beacon advertising with configurable interval and duration
|
||||
- Cross-probe tie-breaker using deterministic NodeAddr comparison
|
||||
- Connection pool with configurable capacity and eviction
|
||||
|
||||
### Fixed
|
||||
|
||||
- Control socket path detection in fipsctl and fipstop now checks for
|
||||
|
||||
230
Cargo.lock
generated
230
Cargo.lock
generated
@@ -220,6 +220,35 @@ dependencies = [
|
||||
"piper",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bluer"
|
||||
version = "0.17.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af68112f5c60196495c8b0eea68349817855f565df5b04b2477916d09fb1a901"
|
||||
dependencies = [
|
||||
"custom_debug",
|
||||
"dbus",
|
||||
"dbus-crossroads",
|
||||
"dbus-tokio",
|
||||
"displaydoc",
|
||||
"futures",
|
||||
"hex",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
"macaddr",
|
||||
"nix 0.29.0",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"pin-project",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.26.3",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.1"
|
||||
@@ -578,14 +607,60 @@ dependencies = [
|
||||
"phf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "custom_debug"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2da7d1ad9567b3e11e877f1d7a0fa0360f04162f94965fc4448fbed41a65298e"
|
||||
dependencies = [
|
||||
"custom_debug_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "custom_debug_derive"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a707ceda8652f6c7624f2be725652e9524c815bf3b9d55a0b2320be2303f9c11"
|
||||
dependencies = [
|
||||
"darling 0.20.11",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
||||
dependencies = [
|
||||
"darling_core 0.20.11",
|
||||
"darling_macro 0.20.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
"darling_core 0.23.0",
|
||||
"darling_macro 0.23.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -601,17 +676,61 @@ dependencies = [
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||
dependencies = [
|
||||
"darling_core 0.20.11",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_core 0.23.0",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dbus"
|
||||
version = "0.9.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "190b6255e8ab55a7b568df5a883e9497edc3e4821c06396612048b430e5ad1e9"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"libc",
|
||||
"libdbus-sys",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dbus-crossroads"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64bff0bd181fba667660276c6b7ebdc50cff37ce593e7adf9e734f89c8f444e8"
|
||||
dependencies = [
|
||||
"dbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dbus-tokio"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "007688d459bc677131c063a3a77fb899526e17b7980f390b69644bdbc41fad13"
|
||||
dependencies = [
|
||||
"dbus",
|
||||
"libc",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deltae"
|
||||
version = "0.3.2"
|
||||
@@ -681,6 +800,17 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "document-features"
|
||||
version = "0.2.12"
|
||||
@@ -786,6 +916,7 @@ name = "fips"
|
||||
version = "0.3.0-dev"
|
||||
dependencies = [
|
||||
"bech32",
|
||||
"bluer",
|
||||
"chacha20poly1305",
|
||||
"clap",
|
||||
"criterion",
|
||||
@@ -1101,7 +1232,7 @@ version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"darling 0.23.0",
|
||||
"indoc",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1189,6 +1320,15 @@ version = "0.2.180"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
|
||||
[[package]]
|
||||
name = "libdbus-sys"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cbe856efeb50e4681f010e9aaa2bf0a644e10139e54cde10fc83a307c23bd9f"
|
||||
dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.9.0"
|
||||
@@ -1264,6 +1404,12 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "macaddr"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baee0bbc17ce759db233beb01648088061bf678383130602a298e6998eedb2d8"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.2.0"
|
||||
@@ -1618,6 +1764,26 @@ dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
|
||||
dependencies = [
|
||||
"pin-project-internal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-internal"
|
||||
version = "1.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.16"
|
||||
@@ -1641,6 +1807,12 @@ dependencies = [
|
||||
"futures-io",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "plotters"
|
||||
version = "0.3.7"
|
||||
@@ -1809,7 +1981,7 @@ dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"kasuari",
|
||||
"lru",
|
||||
"strum",
|
||||
"strum 0.27.2",
|
||||
"thiserror 2.0.18",
|
||||
"unicode-segmentation",
|
||||
"unicode-truncate",
|
||||
@@ -1861,7 +2033,7 @@ dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"line-clipping",
|
||||
"ratatui-core",
|
||||
"strum",
|
||||
"strum 0.27.2",
|
||||
"time",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
@@ -2191,13 +2363,35 @@ version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||
dependencies = [
|
||||
"strum_macros 0.26.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.27.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
"strum_macros 0.27.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2240,6 +2434,17 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.24.0"
|
||||
@@ -2435,6 +2640,17 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.18"
|
||||
|
||||
15
Cargo.toml
15
Cargo.toml
@@ -9,8 +9,9 @@ repository = "https://github.com/jmcorgan/fips"
|
||||
readme = "README.md"
|
||||
|
||||
[features]
|
||||
default = ["tui"]
|
||||
default = ["tui", "ble"]
|
||||
tui = ["dep:ratatui"]
|
||||
ble = ["dep:bluer"]
|
||||
|
||||
[dependencies]
|
||||
ratatui = { version = "0.30", optional = true }
|
||||
@@ -37,6 +38,7 @@ futures = "0.3"
|
||||
simple-dns = "0.11.2"
|
||||
socket2 = { version = "0.6.2", features = ["all"] }
|
||||
tokio-socks = "0.5"
|
||||
bluer = { version = "0.17", features = ["bluetoothd", "l2cap"], optional = true }
|
||||
|
||||
[package.metadata.deb]
|
||||
maintainer = "Johnathan Corgan <jcorgan@corganlabs.com>"
|
||||
@@ -44,12 +46,14 @@ copyright = "2026 Johnathan Corgan"
|
||||
license-file = ["LICENSE", "0"]
|
||||
section = "net"
|
||||
priority = "optional"
|
||||
depends = "libc6, systemd"
|
||||
depends = "libc6, systemd, libdbus-1-3"
|
||||
recommends = "bluez"
|
||||
extended-description = """\
|
||||
FIPS is a distributed, decentralized network routing protocol for mesh \
|
||||
nodes connecting over arbitrary transports including UDP, TCP, and Ethernet. \
|
||||
It provides encrypted peer-to-peer connectivity with automatic key management, \
|
||||
TUN-based virtual networking, and .fips DNS resolution."""
|
||||
nodes connecting over arbitrary transports including UDP, TCP, Ethernet, \
|
||||
Tor, and Bluetooth (BLE). It provides encrypted peer-to-peer connectivity \
|
||||
with automatic key management, TUN-based virtual networking, and .fips DNS \
|
||||
resolution."""
|
||||
maintainer-scripts = "packaging/debian/"
|
||||
assets = [
|
||||
["target/release/fips", "/usr/bin/", "755"],
|
||||
@@ -66,6 +70,7 @@ conf-files = ["/etc/fips/fips.yaml", "/etc/fips/hosts"]
|
||||
[dev-dependencies]
|
||||
tempfile = "3.15"
|
||||
criterion = { version = "0.8.2", features = ["html_reports"] }
|
||||
tokio = { version = "1", features = ["test-util"] }
|
||||
|
||||
[[bin]]
|
||||
name = "fipsctl"
|
||||
|
||||
13
README.md
13
README.md
@@ -38,8 +38,8 @@ endpoints.
|
||||
|
||||
- **Self-organizing mesh routing** — spanning tree coordinates with bloom
|
||||
filter guided discovery, no global routing tables
|
||||
- **Multi-transport** — UDP, TCP, Ethernet, and Tor today; designed for
|
||||
Bluetooth, serial, and radio
|
||||
- **Multi-transport** — UDP, TCP, Ethernet, Tor, and Bluetooth (BLE L2CAP)
|
||||
today; designed for serial and radio
|
||||
- **Noise encryption** — hop-by-hop link encryption (IK) plus independent
|
||||
end-to-end session encryption (XK), with periodic rekey for forward secrecy
|
||||
- **Nostr-native identity** — secp256k1 keypairs as node addresses, no
|
||||
@@ -65,6 +65,10 @@ cargo build --release
|
||||
|
||||
Requires Rust 1.85+ (edition 2024) and Linux with TUN support.
|
||||
|
||||
The BLE transport (enabled by default) requires BlueZ and libdbus. On
|
||||
Debian/Ubuntu: `sudo apt install bluez libdbus-1-dev`. To build without BLE:
|
||||
`cargo build --release --no-default-features --features tui`.
|
||||
|
||||
## Installation
|
||||
|
||||
After building, choose one of the following methods to install.
|
||||
@@ -240,7 +244,7 @@ testing/ Docker-based integration test harnesses
|
||||
## Status & Roadmap
|
||||
|
||||
FIPS is at **v0.2.0**. The core protocol works end-to-end over UDP, TCP,
|
||||
Ethernet, and Tor with a small live mesh of deployed nodes.
|
||||
Ethernet, Tor, and Bluetooth (BLE) with a small live mesh of deployed nodes.
|
||||
|
||||
### What works today
|
||||
|
||||
@@ -253,7 +257,7 @@ Ethernet, and Tor with a small live mesh of deployed nodes.
|
||||
- Static hostname mapping (`/etc/fips/hosts`) with auto-reload
|
||||
- Per-link metrics (RTT, loss, jitter, goodput) and mesh size estimation
|
||||
- ECN congestion signaling (hop-by-hop CE relay, IPv6 CE marking, kernel drop detection)
|
||||
- UDP, TCP, Ethernet, and Tor transports (SOCKS5 outbound + directory-mode onion service inbound)
|
||||
- UDP, TCP, Ethernet, Tor, and BLE transports (BLE via L2CAP CoC with per-link MTU negotiation)
|
||||
- Runtime inspection and peer management via `fipsctl` and `fipstop`
|
||||
- Reproducible builds with toolchain pinning and SOURCE_DATE_EPOCH
|
||||
- Debian and systemd tarball packaging
|
||||
@@ -263,7 +267,6 @@ Ethernet, and Tor with a small live mesh of deployed nodes.
|
||||
|
||||
- Peer discovery via Nostr relays (bootstrap without static peer lists)
|
||||
- Native API for FIPS-aware applications (npub:port addressing)
|
||||
- Additional transports (Bluetooth)
|
||||
- Security audit of cryptographic protocols
|
||||
|
||||
### Longer-term
|
||||
|
||||
@@ -82,48 +82,44 @@
|
||||
<!-- === Transport layer === -->
|
||||
|
||||
<!-- Overlay transports -->
|
||||
<rect x="80" y="336" width="150" height="60" class="cat"/>
|
||||
<text x="155" y="349" text-anchor="middle" class="cat">Overlay</text>
|
||||
<rect x="80" y="336" width="216" height="60" class="cat"/>
|
||||
<text x="188" y="349" text-anchor="middle" class="cat">Overlay</text>
|
||||
|
||||
<rect x="92" y="356" width="60" height="30" class="layer xport"/>
|
||||
<text x="122" y="371" text-anchor="middle" font-size="10" font-weight="bold" fill="#e0e0e0">UDP</text>
|
||||
<text x="122" y="381" text-anchor="middle" class="sub">IP</text>
|
||||
|
||||
<rect x="158" y="356" width="60" height="30" class="layer xport"/>
|
||||
<text x="188" y="371" text-anchor="middle" font-size="10" font-weight="bold" fill="#e0e0e0">Tor</text>
|
||||
<text x="188" y="381" text-anchor="middle" class="sub">.onion</text>
|
||||
<text x="188" y="371" text-anchor="middle" font-size="10" font-weight="bold" fill="#e0e0e0">TCP</text>
|
||||
<text x="188" y="381" text-anchor="middle" class="sub">IP</text>
|
||||
|
||||
<rect x="224" y="356" width="60" height="30" class="layer xport"/>
|
||||
<text x="254" y="371" text-anchor="middle" font-size="10" font-weight="bold" fill="#e0e0e0">Tor</text>
|
||||
<text x="254" y="381" text-anchor="middle" class="sub">.onion</text>
|
||||
|
||||
<!-- Shared medium transports -->
|
||||
<rect x="240" y="336" width="300" height="60" class="cat"/>
|
||||
<text x="390" y="349" text-anchor="middle" class="cat">Shared Medium</text>
|
||||
<rect x="306" y="336" width="234" height="60" class="cat"/>
|
||||
<text x="423" y="349" text-anchor="middle" class="cat">Shared Medium</text>
|
||||
|
||||
<rect x="254" y="356" width="60" height="30" class="layer xport"/>
|
||||
<text x="284" y="371" text-anchor="middle" font-size="10" font-weight="bold" fill="#e0e0e0">Ether</text>
|
||||
<text x="284" y="381" text-anchor="middle" class="sub">802.3</text>
|
||||
<rect x="318" y="356" width="60" height="30" class="layer xport"/>
|
||||
<text x="348" y="371" text-anchor="middle" font-size="10" font-weight="bold" fill="#e0e0e0">Ether</text>
|
||||
<text x="348" y="381" text-anchor="middle" class="sub">802.3</text>
|
||||
|
||||
<rect x="320" y="356" width="60" height="30" class="layer xport"/>
|
||||
<text x="350" y="371" text-anchor="middle" font-size="10" font-weight="bold" fill="#e0e0e0">WiFi</text>
|
||||
<text x="350" y="381" text-anchor="middle" class="sub">802.11</text>
|
||||
<rect x="384" y="356" width="60" height="30" class="layer xport"/>
|
||||
<text x="414" y="371" text-anchor="middle" font-size="10" font-weight="bold" fill="#e0e0e0">BLE</text>
|
||||
<text x="414" y="381" text-anchor="middle" class="sub">L2CAP</text>
|
||||
|
||||
<rect x="386" y="356" width="60" height="30" class="layer xport"/>
|
||||
<text x="416" y="371" text-anchor="middle" font-size="10" font-weight="bold" fill="#e0e0e0">BT</text>
|
||||
<text x="416" y="381" text-anchor="middle" class="sub">RFCOMM</text>
|
||||
|
||||
<rect x="452" y="356" width="60" height="30" class="layer xport"/>
|
||||
<text x="482" y="371" text-anchor="middle" font-size="10" font-weight="bold" fill="#e0e0e0">Radio</text>
|
||||
<text x="482" y="381" text-anchor="middle" class="sub">Sat, ...</text>
|
||||
<rect x="450" y="356" width="80" height="30" class="layer xport"/>
|
||||
<text x="490" y="371" text-anchor="middle" font-size="10" font-weight="bold" fill="#e0e0e0">Radio ...</text>
|
||||
<text x="490" y="381" text-anchor="middle" class="sub">future</text>
|
||||
|
||||
<!-- Point-to-point transports -->
|
||||
<rect x="550" y="336" width="150" height="60" class="cat"/>
|
||||
<text x="625" y="349" text-anchor="middle" class="cat">Point-to-Point</text>
|
||||
|
||||
<rect x="562" y="356" width="60" height="30" class="layer xport"/>
|
||||
<text x="592" y="371" text-anchor="middle" font-size="10" font-weight="bold" fill="#e0e0e0">Serial</text>
|
||||
<text x="592" y="381" text-anchor="middle" class="sub">UART</text>
|
||||
|
||||
<rect x="628" y="356" width="60" height="30" class="layer xport"/>
|
||||
<text x="658" y="371" text-anchor="middle" font-size="10" font-weight="bold" fill="#e0e0e0">...</text>
|
||||
<text x="658" y="381" text-anchor="middle" class="sub"></text>
|
||||
<rect x="562" y="356" width="126" height="30" class="layer xport"/>
|
||||
<text x="625" y="371" text-anchor="middle" font-size="10" font-weight="bold" fill="#e0e0e0">Serial ...</text>
|
||||
<text x="625" y="381" text-anchor="middle" class="sub">future</text>
|
||||
|
||||
<!-- === Peer networks below node box === -->
|
||||
<text x="155" y="436" text-anchor="middle" class="sub">Internet / Overlay Peers</text>
|
||||
|
||||
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 7.8 KiB |
@@ -485,6 +485,78 @@ HiddenServiceDir /var/lib/tor/fips
|
||||
HiddenServicePort 8443 127.0.0.1:8444
|
||||
```
|
||||
|
||||
### BLE (`transports.ble.*`)
|
||||
|
||||
Bluetooth Low Energy transport using L2CAP Connection-Oriented Channels.
|
||||
Requires BlueZ and the `ble` Cargo feature flag (default-on). Linux only;
|
||||
guarded by `#[cfg(target_os = "linux")]`. Communicates with BlueZ via D-Bus
|
||||
using the `bluer` crate.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `transports.ble.adapter` | string | `"hci0"` | HCI adapter name |
|
||||
| `transports.ble.psm` | u16 | `0x0085` (133) | L2CAP Protocol/Service Multiplexer |
|
||||
| `transports.ble.mtu` | u16 | `2048` | Default MTU. Actual MTU is negotiated per-link during L2CAP connection setup. |
|
||||
| `transports.ble.max_connections` | usize | `7` | Maximum concurrent BLE connections |
|
||||
| `transports.ble.connect_timeout_ms` | u64 | `10000` | Outbound connect timeout in milliseconds |
|
||||
| `transports.ble.advertise` | bool | `true` | Broadcast BLE beacon advertisements for peer discovery |
|
||||
| `transports.ble.scan` | bool | `true` | Listen for BLE beacon advertisements from other nodes |
|
||||
| `transports.ble.auto_connect` | bool | `false` | Automatically connect to discovered peers |
|
||||
| `transports.ble.accept_connections` | bool | `true` | Accept incoming L2CAP connections |
|
||||
| `transports.ble.scan_interval_secs` | u64 | `10` | Interval between BLE scan cycles |
|
||||
| `transports.ble.beacon_interval_secs` | u64 | `10` | Interval between beacon advertising bursts |
|
||||
| `transports.ble.beacon_duration_secs` | u64 | `3` | Duration of each beacon advertising burst |
|
||||
|
||||
**Address format.** BLE peer addresses use the form
|
||||
`"adapter/device_address"` — for example, `"hci0/AA:BB:CC:DD:EE:FF"`.
|
||||
|
||||
**Scan/probe and tie-breaking.** When `scan` is enabled, the transport
|
||||
periodically scans for BLE beacons from other FIPS nodes. Discovered
|
||||
peers are probed with per-entry random jitter to prevent herd effects
|
||||
when multiple nodes see the same beacon simultaneously. If two nodes
|
||||
probe each other at the same time (cross-probe), a deterministic
|
||||
tie-breaker based on NodeAddr comparison ensures only one connection
|
||||
is established.
|
||||
|
||||
**Connection pool.** The `max_connections` parameter limits the number of
|
||||
concurrent BLE connections. When the pool is full, the least-recently-used
|
||||
connection is evicted to make room for new connections.
|
||||
|
||||
### BLE Example
|
||||
|
||||
A node using BLE for local mesh discovery alongside UDP for internet peers:
|
||||
|
||||
```yaml
|
||||
node:
|
||||
identity:
|
||||
persistent: true
|
||||
|
||||
tun:
|
||||
enabled: true
|
||||
|
||||
transports:
|
||||
udp:
|
||||
bind_addr: "0.0.0.0:2121"
|
||||
ble:
|
||||
adapter: "hci0"
|
||||
advertise: true
|
||||
scan: true
|
||||
auto_connect: true
|
||||
accept_connections: true
|
||||
|
||||
peers:
|
||||
- npub: "npub1abc..."
|
||||
alias: "internet-peer"
|
||||
addresses:
|
||||
- transport: udp
|
||||
addr: "203.0.113.5:2121"
|
||||
connect_policy: auto_connect
|
||||
```
|
||||
|
||||
BLE peers on the local radio range are discovered automatically via
|
||||
beacons — no static peer entries needed. Internet peers still require
|
||||
explicit configuration.
|
||||
|
||||
## Peers (`peers[]`)
|
||||
|
||||
Static peer list. Each entry defines a peer to connect to.
|
||||
@@ -493,8 +565,8 @@ Static peer list. Each entry defines a peer to connect to.
|
||||
|-----------|------|---------|-------------|
|
||||
| `peers[].npub` | string | *(required)* | Peer's Nostr public key (npub-encoded) |
|
||||
| `peers[].alias` | string | *(none)* | Human-readable name for logging |
|
||||
| `peers[].addresses[].transport` | string | *(required)* | Transport type: `udp`, `tcp`, `ethernet`, or `tor` |
|
||||
| `peers[].addresses[].addr` | string | *(required)* | Transport address. UDP/TCP: `"host:port"` (IP or DNS hostname). Ethernet: `"interface/mac"` (e.g., `"eth0/aa:bb:cc:dd:ee:ff"`). Tor: `".onion:port"` or `"host:port"` |
|
||||
| `peers[].addresses[].transport` | string | *(required)* | Transport type: `udp`, `tcp`, `ethernet`, `tor`, or `ble` |
|
||||
| `peers[].addresses[].addr` | string | *(required)* | Transport address. UDP/TCP: `"host:port"` (IP or DNS hostname). Ethernet: `"interface/mac"` (e.g., `"eth0/aa:bb:cc:dd:ee:ff"`). BLE: `"adapter/device_address"` (e.g., `"hci0/AA:BB:CC:DD:EE:FF"`). Tor: `".onion:port"` or `"host:port"` |
|
||||
| `peers[].addresses[].priority` | u8 | `100` | Address priority (lower = preferred) |
|
||||
| `peers[].connect_policy` | string | `"auto_connect"` | Connection policy: `auto_connect`, `on_demand`, or `manual` |
|
||||
| `peers[].auto_reconnect` | bool | `true` | Automatically reconnect after MMP link-dead removal (exponential backoff, unlimited retries) |
|
||||
@@ -720,6 +792,19 @@ transports:
|
||||
# # hostname_file: "/var/lib/tor/fips/hostname"
|
||||
# # bind_addr: "127.0.0.1:8444"
|
||||
# # max_inbound_connections: 64
|
||||
# ble: # uncomment to enable BLE transport (Linux only, requires BlueZ)
|
||||
# adapter: "hci0" # HCI adapter name
|
||||
# psm: 0x0085 # L2CAP PSM (133)
|
||||
# mtu: 2048 # default MTU (negotiated per-link)
|
||||
# max_connections: 7 # max concurrent BLE connections
|
||||
# connect_timeout_ms: 10000 # outbound connect timeout
|
||||
# advertise: true # broadcast BLE beacons
|
||||
# scan: true # listen for BLE beacons
|
||||
# auto_connect: false # connect to discovered peers
|
||||
# accept_connections: true # accept incoming L2CAP connections
|
||||
# scan_interval_secs: 10 # interval between scan cycles
|
||||
# beacon_interval_secs: 10 # interval between beacon bursts
|
||||
# beacon_duration_secs: 3 # duration of each beacon burst
|
||||
|
||||
peers: # static peer list
|
||||
# - npub: "npub1..."
|
||||
|
||||
@@ -524,9 +524,10 @@ forwarding, a publicly addressed peer, or relay through other mesh nodes.
|
||||
UDP hole punching and relay-assisted NAT traversal are potential future
|
||||
mechanisms but are not part of the current design.
|
||||
|
||||
> **Implementation status**: UDP/IP, TCP/IP, Ethernet, and Tor
|
||||
> (SOCKS5 outbound + directory-mode inbound via onion service)
|
||||
> transports are implemented. All others are future directions.
|
||||
> **Implementation status**: UDP/IP, TCP/IP, Ethernet, Tor
|
||||
> (SOCKS5 outbound + directory-mode inbound via onion service),
|
||||
> and Bluetooth (BLE L2CAP CoC) transports are implemented.
|
||||
> All others are future directions.
|
||||
|
||||
See [fips-transport-layer.md](fips-transport-layer.md) for the full transport
|
||||
layer specification.
|
||||
|
||||
@@ -37,6 +37,15 @@ transports:
|
||||
# auto_connect: true
|
||||
# accept_connections: true
|
||||
|
||||
# Bluetooth Low Energy transport — requires BlueZ and the 'ble' feature.
|
||||
# ble:
|
||||
# adapter: "hci0"
|
||||
# mtu: 2048
|
||||
# advertise: true
|
||||
# scan: true
|
||||
# auto_connect: true
|
||||
# accept_connections: true
|
||||
|
||||
peers: []
|
||||
# Static peers for bootstrapping (UDP or TCP):
|
||||
# - npub: "npub1..."
|
||||
|
||||
@@ -62,7 +62,32 @@ transports:
|
||||
accept_connections: true
|
||||
```
|
||||
|
||||
### 3. Static Peers
|
||||
### 3. Bluetooth Transport
|
||||
|
||||
If using BLE for local mesh discovery, the FIPS binary must be built with
|
||||
the `ble` feature (enabled by default). BlueZ must be installed and running:
|
||||
|
||||
```bash
|
||||
sudo apt install bluez
|
||||
sudo systemctl enable --now bluetooth
|
||||
```
|
||||
|
||||
Add your service user to the `bluetooth` group, or run with
|
||||
`CAP_NET_ADMIN` + `CAP_NET_RAW` capabilities.
|
||||
|
||||
Configure BLE in the transports section:
|
||||
|
||||
```yaml
|
||||
transports:
|
||||
ble:
|
||||
adapter: "hci0"
|
||||
advertise: true
|
||||
scan: true
|
||||
auto_connect: true
|
||||
accept_connections: true
|
||||
```
|
||||
|
||||
### 4. Static Peers
|
||||
|
||||
For bootstrapping over UDP or TCP, add known peers:
|
||||
|
||||
@@ -76,7 +101,7 @@ peers:
|
||||
connect_policy: auto_connect
|
||||
```
|
||||
|
||||
### 4. DNS Resolver (optional, requires systemd-resolved)
|
||||
### 5. DNS Resolver (optional, requires systemd-resolved)
|
||||
|
||||
FIPS includes a DNS responder for `.fips` domain names (port 5354).
|
||||
On systems running `systemd-resolved`, the installer automatically enables
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
src/node/tests/ble.rs
Normal file
277
src/node/tests/ble.rs
Normal 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;
|
||||
}
|
||||
@@ -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
src/transport/ble/addr.rs
Normal file
185
src/transport/ble/addr.rs
Normal 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
src/transport/ble/discovery.rs
Normal file
126
src/transport/ble/discovery.rs
Normal 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
src/transport/ble/io.rs
Normal file
774
src/transport/ble/io.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
1183
src/transport/ble/mod.rs
Normal file
1183
src/transport/ble/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
294
src/transport/ble/pool.rs
Normal file
294
src/transport/ble/pool.rs
Normal 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
src/transport/ble/stats.rs
Normal file
155
src/transport/ble/stats.rs
Normal 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,
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
testing/ble/.gitignore
vendored
Normal file
1
testing/ble/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
target/
|
||||
996
testing/ble/Cargo.lock
generated
Normal file
996
testing/ble/Cargo.lock
generated
Normal file
@@ -0,0 +1,996 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "ble-spike"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bluer",
|
||||
"futures",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bluer"
|
||||
version = "0.17.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af68112f5c60196495c8b0eea68349817855f565df5b04b2477916d09fb1a901"
|
||||
dependencies = [
|
||||
"custom_debug",
|
||||
"dbus",
|
||||
"dbus-crossroads",
|
||||
"dbus-tokio",
|
||||
"displaydoc",
|
||||
"futures",
|
||||
"hex",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
"macaddr",
|
||||
"nix",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"pin-project",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "custom_debug"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2da7d1ad9567b3e11e877f1d7a0fa0360f04162f94965fc4448fbed41a65298e"
|
||||
dependencies = [
|
||||
"custom_debug_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "custom_debug_derive"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a707ceda8652f6c7624f2be725652e9524c815bf3b9d55a0b2320be2303f9c11"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dbus"
|
||||
version = "0.9.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b3aa68d7e7abee336255bd7248ea965cc393f3e70411135a6f6a4b651345d4"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"libc",
|
||||
"libdbus-sys",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dbus-crossroads"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64bff0bd181fba667660276c6b7ebdc50cff37ce593e7adf9e734f89c8f444e8"
|
||||
dependencies = [
|
||||
"dbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dbus-tokio"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "007688d459bc677131c063a3a77fb899526e17b7980f390b69644bdbc41fad13"
|
||||
dependencies = [
|
||||
"dbus",
|
||||
"libc",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "id-arena"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.16.1",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.91"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.183"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||
|
||||
[[package]]
|
||||
name = "libdbus-sys"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043"
|
||||
dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "macaddr"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baee0bbc17ce759db233beb01648088061bf678383130602a298e6998eedb2d8"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517"
|
||||
dependencies = [
|
||||
"pin-project-internal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-internal"
|
||||
version = "1.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.50.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.2+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip3"
|
||||
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-encoder"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
||||
dependencies = [
|
||||
"leb128fmt",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-metadata"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap",
|
||||
"wasm-encoder",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
dependencies = [
|
||||
"wit-bindgen-rust-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-core"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"indexmap",
|
||||
"prettyplease",
|
||||
"syn",
|
||||
"wasm-metadata",
|
||||
"wit-bindgen-core",
|
||||
"wit-component",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust-macro"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wit-bindgen-core",
|
||||
"wit-bindgen-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-component"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"wasm-encoder",
|
||||
"wasm-metadata",
|
||||
"wasmparser",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-parser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"id-arena",
|
||||
"indexmap",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"unicode-xid",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
14
testing/ble/Cargo.toml
Normal file
14
testing/ble/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "ble-spike"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "ble_spike"
|
||||
path = "ble_spike.rs"
|
||||
|
||||
[dependencies]
|
||||
bluer = { version = "0.17", features = ["bluetoothd", "l2cap"] }
|
||||
tokio = { version = "1", features = ["rt", "macros", "sync", "time"] }
|
||||
futures = "0.3"
|
||||
363
testing/ble/ble_spike.rs
Normal file
363
testing/ble/ble_spike.rs
Normal file
@@ -0,0 +1,363 @@
|
||||
//! BLE L2CAP spike — validates bluer API assumptions for the BleIo trait.
|
||||
//!
|
||||
//! Usage:
|
||||
//! ble_spike listen # Advertise + listen for L2CAP connections
|
||||
//! ble_spike connect <MAC> # Scan + connect to a listening peer
|
||||
//! ble_spike info # Print adapter info only
|
||||
//!
|
||||
//! Build: cd testing/ble && cargo build
|
||||
//!
|
||||
//! Test flow (two machines with BLE adapters):
|
||||
//! host-a$ ./ble_spike listen
|
||||
//! host-b$ ./ble_spike connect <host-a-MAC>
|
||||
|
||||
mod spike {
|
||||
use bluer::l2cap::{SeqPacket, SeqPacketListener, Socket, SocketAddr};
|
||||
use bluer::{adv::Advertisement, AdapterEvent, Address, AddressType, DiscoveryFilter, DiscoveryTransport};
|
||||
use futures::StreamExt;
|
||||
use std::collections::BTreeSet;
|
||||
use std::pin::pin;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
/// FIPS BLE service UUID.
|
||||
/// Derived from SHA-256("FIPS: welcome to cryptoanarchy") with UUID v4
|
||||
/// version/variant bits applied.
|
||||
const FIPS_SERVICE_UUID: bluer::Uuid = bluer::Uuid::from_u128(
|
||||
0x9c90_b790_2cc5_42c0_9f87_c9cc_4064_8f4c,
|
||||
);
|
||||
|
||||
/// L2CAP PSM for FIPS connections (dynamic range).
|
||||
const FIPS_PSM: u16 = 0x0085;
|
||||
|
||||
/// Desired L2CAP CoC MTU (requested during negotiation).
|
||||
/// Override with FIPS_BLE_MTU env var for testing asymmetric MTU.
|
||||
fn desired_mtu() -> u16 {
|
||||
std::env::var("FIPS_BLE_MTU")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(2048)
|
||||
}
|
||||
|
||||
/// Test message sent over L2CAP.
|
||||
const TEST_MSG: &[u8] = b"FIPS BLE spike test";
|
||||
|
||||
pub async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let cmd = args.get(1).map(|s| s.as_str()).unwrap_or("info");
|
||||
|
||||
let session = bluer::Session::new().await?;
|
||||
let adapter = session.default_adapter().await?;
|
||||
adapter.set_powered(true).await?;
|
||||
|
||||
let addr = adapter.address().await?;
|
||||
let name = adapter.name().to_string();
|
||||
println!("Adapter: {} ({})", name, addr);
|
||||
println!("Address type: {:?}", adapter.address_type().await?);
|
||||
|
||||
match cmd {
|
||||
"info" => {
|
||||
println!("Adapter info only. Use 'listen' or 'connect <MAC>' for testing.");
|
||||
}
|
||||
"listen" => listen(&adapter).await?,
|
||||
"connect" => {
|
||||
let mac = args.get(2).ok_or("usage: ble_spike connect <MAC>")?;
|
||||
let target: Address = mac.parse().map_err(|_| format!("invalid MAC: {mac}"))?;
|
||||
connect(&adapter, target).await?;
|
||||
}
|
||||
"sink" => sink(&adapter).await?,
|
||||
"throughput" => {
|
||||
let mac = args.get(2).ok_or("usage: ble_spike throughput <MAC>")?;
|
||||
let target: Address = mac.parse().map_err(|_| format!("invalid MAC: {mac}"))?;
|
||||
throughput(&adapter, target).await?;
|
||||
}
|
||||
other => {
|
||||
eprintln!("unknown command: {other}");
|
||||
eprintln!("usage: ble_spike [info|listen|connect|sink|throughput] <MAC>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn listen(adapter: &bluer::Adapter) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Start advertising the FIPS UUID
|
||||
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-spike".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let adv_handle = adapter.advertise(adv).await?;
|
||||
println!("Advertising FIPS UUID: {FIPS_SERVICE_UUID}");
|
||||
|
||||
// Bind L2CAP SeqPacket listener with requested MTU
|
||||
let local_sa = SocketAddr::new(
|
||||
adapter.address().await?,
|
||||
AddressType::LePublic,
|
||||
FIPS_PSM,
|
||||
);
|
||||
let listener = SeqPacketListener::bind(local_sa).await?;
|
||||
listener.as_ref().set_recv_mtu(desired_mtu())?;
|
||||
let mtu = desired_mtu();
|
||||
println!("Listening on PSM 0x{FIPS_PSM:04X} (requested MTU: {mtu})");
|
||||
println!("Waiting for connection... (Ctrl-C to stop)");
|
||||
|
||||
// Accept one connection
|
||||
let (conn, peer_sa) = listener.accept().await?;
|
||||
println!("Accepted connection from: {}", peer_sa.addr);
|
||||
println!(" Send MTU: {}", conn.send_mtu()?);
|
||||
println!(" Recv MTU: {}", conn.recv_mtu()?);
|
||||
|
||||
// Receive data
|
||||
let mut buf = vec![0u8; 4096];
|
||||
let n = conn.recv(&mut buf).await?;
|
||||
println!(" Received {} bytes: {:?}", n, String::from_utf8_lossy(&buf[..n]));
|
||||
|
||||
// Send response
|
||||
let response = b"FIPS BLE spike response";
|
||||
let sent = conn.send(response).await?;
|
||||
println!(" Sent {} bytes", sent);
|
||||
|
||||
// Receive and echo large payload
|
||||
let n = timeout(Duration::from_secs(5), conn.recv(&mut buf)).await??;
|
||||
println!(" Received large payload: {} bytes", n);
|
||||
let echoed = conn.send(&buf[..n]).await?;
|
||||
println!(" Echoed {} bytes", echoed);
|
||||
|
||||
// Wait a moment before cleanup
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
drop(adv_handle);
|
||||
println!("Done.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn connect(
|
||||
adapter: &bluer::Adapter,
|
||||
target: Address,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Optional: scan for the target first to verify it's advertising
|
||||
println!("Scanning for target {target}...");
|
||||
let filter = DiscoveryFilter {
|
||||
transport: DiscoveryTransport::Le,
|
||||
..Default::default()
|
||||
};
|
||||
adapter.set_discovery_filter(filter).await?;
|
||||
|
||||
let events = adapter.discover_devices().await?;
|
||||
let mut events = pin!(events);
|
||||
|
||||
let found = timeout(Duration::from_secs(10), async {
|
||||
while let Some(event) = events.next().await {
|
||||
if let AdapterEvent::DeviceAdded(addr) = event {
|
||||
println!(" Discovered: {addr}");
|
||||
if addr == target {
|
||||
// Check if it has our UUID
|
||||
if let Ok(device) = adapter.device(addr) {
|
||||
if let Ok(Some(uuids)) = device.uuids().await {
|
||||
if uuids.contains(&FIPS_SERVICE_UUID) {
|
||||
println!(" -> Found FIPS service UUID!");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(" -> Target found (no UUID filter match, connecting anyway)");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
})
|
||||
.await;
|
||||
|
||||
match found {
|
||||
Ok(true) => println!("Target found."),
|
||||
Ok(false) => println!("Scan ended without finding target, trying connect anyway..."),
|
||||
Err(_) => println!("Scan timeout, trying connect anyway..."),
|
||||
}
|
||||
|
||||
// Stop scanning before connecting
|
||||
drop(events);
|
||||
|
||||
// Connect via L2CAP SeqPacket with requested MTU
|
||||
let target_sa = SocketAddr::new(target, AddressType::LePublic, FIPS_PSM);
|
||||
let mtu = desired_mtu();
|
||||
println!("Connecting to {target} on PSM 0x{FIPS_PSM:04X} (requested MTU: {mtu})...");
|
||||
|
||||
let socket = Socket::<SeqPacket>::new_seq_packet()?;
|
||||
socket.bind(SocketAddr::any_le())?;
|
||||
socket.set_recv_mtu(desired_mtu())?;
|
||||
let conn = timeout(Duration::from_secs(15), socket.connect(target_sa)).await??;
|
||||
println!("Connected!");
|
||||
println!(" Send MTU: {}", conn.send_mtu()?);
|
||||
println!(" Recv MTU: {}", conn.recv_mtu()?);
|
||||
println!(" Peer: {}", conn.peer_addr()?.addr);
|
||||
|
||||
// Send small test message
|
||||
let sent = conn.send(TEST_MSG).await?;
|
||||
println!(" Sent {} bytes: {:?}", sent, String::from_utf8_lossy(TEST_MSG));
|
||||
|
||||
// Receive response
|
||||
let mut buf = vec![0u8; 4096];
|
||||
let n = timeout(Duration::from_secs(5), conn.recv(&mut buf)).await??;
|
||||
println!(" Received {} bytes: {:?}", n, String::from_utf8_lossy(&buf[..n]));
|
||||
|
||||
// Send large payload to test MTU
|
||||
let send_mtu = conn.send_mtu()?;
|
||||
let large = vec![0xAB_u8; send_mtu];
|
||||
let sent = conn.send(&large).await?;
|
||||
println!(" Sent large payload: {} bytes (send_mtu={})", sent, send_mtu);
|
||||
|
||||
let n = timeout(Duration::from_secs(5), conn.recv(&mut buf)).await??;
|
||||
println!(" Received large echo: {} bytes", n);
|
||||
|
||||
println!("Done.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sink mode: advertise, accept connection, receive and count bytes for 10s.
|
||||
async fn sink(adapter: &bluer::Adapter) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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-sink".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let _adv_handle = adapter.advertise(adv).await?;
|
||||
|
||||
let local_sa = SocketAddr::new(
|
||||
adapter.address().await?,
|
||||
AddressType::LePublic,
|
||||
FIPS_PSM,
|
||||
);
|
||||
let listener = SeqPacketListener::bind(local_sa).await?;
|
||||
listener.as_ref().set_recv_mtu(desired_mtu())?;
|
||||
println!("Sink waiting for connection...");
|
||||
|
||||
let (conn, peer_sa) = listener.accept().await?;
|
||||
println!(
|
||||
"Connected from {} (send_mtu={}, recv_mtu={})",
|
||||
peer_sa.addr,
|
||||
conn.send_mtu()?,
|
||||
conn.recv_mtu()?,
|
||||
);
|
||||
|
||||
let mut buf = vec![0u8; 8192];
|
||||
let mut total_bytes: u64 = 0;
|
||||
let mut total_msgs: u64 = 0;
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
loop {
|
||||
match conn.recv(&mut buf).await {
|
||||
Ok(0) => break,
|
||||
Ok(n) => {
|
||||
total_bytes += n as u64;
|
||||
total_msgs += 1;
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
let secs = elapsed.as_secs_f64();
|
||||
let kbps = (total_bytes as f64 * 8.0) / (secs * 1000.0);
|
||||
println!(
|
||||
"Received {} bytes in {} messages over {:.2}s ({:.1} kbps)",
|
||||
total_bytes, total_msgs, secs, kbps,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Throughput mode: connect and blast data for 10 seconds.
|
||||
async fn throughput(
|
||||
adapter: &bluer::Adapter,
|
||||
target: Address,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Scan for target
|
||||
println!("Scanning for {target}...");
|
||||
let filter = DiscoveryFilter {
|
||||
transport: DiscoveryTransport::Le,
|
||||
..Default::default()
|
||||
};
|
||||
adapter.set_discovery_filter(filter).await?;
|
||||
let events = adapter.discover_devices().await?;
|
||||
let mut events = pin!(events);
|
||||
|
||||
let _ = timeout(Duration::from_secs(10), async {
|
||||
while let Some(event) = events.next().await {
|
||||
if let AdapterEvent::DeviceAdded(addr) = event {
|
||||
if addr == target {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
drop(events);
|
||||
|
||||
// Connect with MTU
|
||||
let target_sa = SocketAddr::new(target, AddressType::LePublic, FIPS_PSM);
|
||||
let socket = Socket::<SeqPacket>::new_seq_packet()?;
|
||||
socket.bind(SocketAddr::any_le())?;
|
||||
socket.set_recv_mtu(desired_mtu())?;
|
||||
let conn = timeout(Duration::from_secs(15), socket.connect(target_sa)).await??;
|
||||
|
||||
let send_mtu = conn.send_mtu()?;
|
||||
println!(
|
||||
"Connected (send_mtu={}, recv_mtu={}). Sending for 10s...",
|
||||
send_mtu,
|
||||
conn.recv_mtu()?,
|
||||
);
|
||||
|
||||
let payload = vec![0xAB_u8; send_mtu];
|
||||
let mut total_bytes: u64 = 0;
|
||||
let mut total_msgs: u64 = 0;
|
||||
let mut errors: u64 = 0;
|
||||
let start = std::time::Instant::now();
|
||||
let duration = Duration::from_secs(10);
|
||||
|
||||
while start.elapsed() < duration {
|
||||
match conn.send(&payload).await {
|
||||
Ok(n) => {
|
||||
total_bytes += n as u64;
|
||||
total_msgs += 1;
|
||||
}
|
||||
Err(_) => {
|
||||
errors += 1;
|
||||
if errors > 10 {
|
||||
println!("Too many errors, stopping.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
let secs = elapsed.as_secs_f64();
|
||||
let kbps = (total_bytes as f64 * 8.0) / (secs * 1000.0);
|
||||
println!(
|
||||
"Sent {} bytes in {} messages over {:.2}s ({:.1} kbps, {} errors)",
|
||||
total_bytes, total_msgs, secs, kbps, errors,
|
||||
);
|
||||
|
||||
// Close cleanly
|
||||
let _ = conn.shutdown(std::net::Shutdown::Both);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() {
|
||||
if let Err(e) = spike::run().await {
|
||||
eprintln!("Error: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user