Add BLE L2CAP transport with scan-based auto-connect

BLE transport implementation using L2CAP Connection-Oriented Channels
(SeqPacket mode) via the bluer crate, behind cfg(feature = "ble").

Core transport:
- BleTransport<I> generic over BleIo trait (BluerIo prod, MockBleIo test)
- Connection pool with priority eviction (static > discovered, max 7)
- Connect-on-send via connect_inline() matching TCP behavior
- Per-connection receive loops with pool cleanup on disconnect

Discovery and probing:
- Combined scan_probe_loop using select! over scanner events and a
  BinaryHeap delay queue with per-entry random jitter (0-5s) to prevent
  herd effects when multiple nodes see the same beacon simultaneously
- Pre-handshake pubkey exchange ([0x00][pubkey:32]) for IK identity
- Cross-probe tie-breaker: smaller NodeAddr's outbound wins (same
  convention as FMP/FSP rekey dual-initiation)
- Probed peers reported to DiscoveryBuffer; pool fills through normal
  node-layer auto-connect -> send_async -> connect_inline path

Beacon management:
- Periodic advertising: 1s burst every 30s (configurable via
  beacon_interval_secs / beacon_duration_secs)
- FIPS service UUID for scan filtering

Configuration (all fields optional with defaults):
- adapter, psm, mtu, max_connections, connect_timeout_ms
- advertise, scan, auto_connect, accept_connections
- beacon_interval_secs (30), beacon_duration_secs (1)

Hardware validated with two BLE nodes:
- 2048-byte MTU, ~60-160ms RTT, zero-config auto-connect
- BLE spike tool at testing/ble/ for standalone adapter validation

42 unit tests + 4 node-level integration tests, all CI-compatible
via MockBleIo (no hardware required). tokio test-util added for
time-dependent scan/probe tests.
This commit is contained in:
Johnathan Corgan
2026-03-25 04:21:46 +00:00
parent d3385b902a
commit 89352d3218
27 changed files with 5098 additions and 54 deletions
+22 -26
View File
@@ -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

+87 -2
View File
@@ -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..."
+4 -3
View File
@@ -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.