feat: reliable Bluetooth pairing, reconnection, and A2DP setup
Four fixes, each a root cause verified on hardware (AX211 + Bose QC Ultra):
1. Link Key Request Reply TRUNCATED: the pending-command queue's params
buffer was 16 bytes; the reply is 22 (addr 6 + key 16). The controller
got 10 key bytes -> every stored-key reconnection failed authentication
(status 5) since 2026-06-03 (c119a70). Fresh pairings never touch this
path, which kept the bug perfectly disguised as a headset quirk.
2. Secure Connections host support (0x0C7A) now enabled: bonds are minted
as P-256 (Type=7), interoperable with BlueZ's, and SC-bonded peers can
actually authenticate us.
3. Never write the BD_ADDR override (0xFC31) with the factory address:
it desyncs the firmware's crypto address from the on-air one and ALL
SSP pairing fails with status 5. (The spoofing feature itself was
already known-cosmetic: the baseband answers pages on the factory
address regardless.) import-bluez-bond.sh now removes the override.
4. A2DP channel setup: wait for Encryption Change before dialing L2CAP
(post-SSP sinks ignore unencrypted CONN_REQ), and LISTEN 2.5s first --
on reconnection the sink dials AVDTP itself and ignores our dials while
doing so. Ends the historical connRsp=FFFF retry-then-give-up failures.
Plus: queued security replies now log delivery + controller status.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,4 +12,4 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#define MONTAUK_BUILD_NUMBER 31
|
||||
#define MONTAUK_BUILD_NUMBER 36
|
||||
|
||||
@@ -971,9 +971,64 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
return true; // channel configured + query sent; proceed to AVDTP
|
||||
}
|
||||
|
||||
// Wait for the ACL link to be ENCRYPTED before dialing any L2CAP channel.
|
||||
// Post-SSP, the sink is REQUIRED to ignore unencrypted L2CAP -- dialing
|
||||
// SDP/AVDTP before Encryption Change lands is a race we historically lost
|
||||
// often (connRsp stays 0xFFFF on every attempt; "toggle the headphones"
|
||||
// sometimes won the race by accident). Pump ProcessPendingCommands here:
|
||||
// the SET_CONN_ENCRYPT that MAKES encryption happen sits in the pending
|
||||
// queue and otherwise only goes out when the idle loop gets around to it.
|
||||
static bool WaitEncrypted(uint32_t timeoutMs) {
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||
Xhci::PollEvents();
|
||||
Hci::DrainEvents();
|
||||
Hci::ProcessPendingCommands();
|
||||
auto* c = Hci::GetActiveConnection();
|
||||
if (c && c->Encrypted) return true;
|
||||
for (int j = 0; j < 100; j++) asm volatile("" ::: "memory");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool StartSource(uint32_t timeoutMs) {
|
||||
constexpr int kMaxAttempts = 4;
|
||||
|
||||
if (WaitEncrypted(3000)) {
|
||||
KernelLogStream(OK, "BT-A2DP") << "Link encrypted; dialing channels";
|
||||
} else {
|
||||
// Some sinks may not encrypt (legacy); proceed as before, but say so.
|
||||
KernelLogStream(WARNING, "BT-A2DP")
|
||||
<< "Link not encrypted after 3s; dialing anyway";
|
||||
}
|
||||
|
||||
// Give the sink first move. On RECONNECTION (bonded device) sinks --
|
||||
// Bose QC included -- commonly dial the AVDTP channels THEMSELVES
|
||||
// right after encryption, and ignore the source's own CONN_REQs while
|
||||
// their setup is in flight (observed: connRsp stays 0xFFFF on every
|
||||
// dialed attempt, zero inbound traffic... because we never stopped
|
||||
// transmitting long enough to receive). Listen briefly before
|
||||
// dialing; fresh pairings are source-driven and just spend the wait.
|
||||
g_sigCid = 0;
|
||||
g_mediaCid = 0;
|
||||
g_state = State::Idle;
|
||||
g_txLabel = 1;
|
||||
g_avdtpResponseReady = false;
|
||||
{
|
||||
uint64_t lStart = Timekeeping::GetMilliseconds();
|
||||
while (Timekeeping::GetMilliseconds() - lStart < 2500) {
|
||||
Xhci::PollEvents();
|
||||
Hci::DrainEvents();
|
||||
Hci::ProcessPendingCommands();
|
||||
if (g_sigCid != 0) break;
|
||||
for (int j = 0; j < 100; j++) asm volatile("" ::: "memory");
|
||||
}
|
||||
}
|
||||
if (g_sigCid != 0) {
|
||||
KernelLogStream(OK, "BT-A2DP") << "Sink opened AVDTP to us (cid="
|
||||
<< base::hex << (uint64_t)g_sigCid << base::dec << ")";
|
||||
}
|
||||
|
||||
// Connection phase, retried. A sink commonly ignores the very first
|
||||
// L2CAP CONN_REQ that lands right after Encryption Change (connRsp stays
|
||||
// 0xFFFF, the headset never answers). Re-dial up to kMaxAttempts. We
|
||||
@@ -983,8 +1038,10 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
// (connRsp!=0xFFFF) we do NOT loop -- that's the config phase, surfaced
|
||||
// by its own logs. We never reset the CID allocator, so each retry uses
|
||||
// a fresh local CID and a late response for an old one cannot cross-wire.
|
||||
for (int attempt = 0; attempt < kMaxAttempts; attempt++) {
|
||||
g_sigCid = 0;
|
||||
// Dial only while no signaling channel exists in either direction;
|
||||
// an inbound one (from the listen phase above, or landing between
|
||||
// retries) short-circuits straight to negotiation.
|
||||
for (int attempt = 0; attempt < kMaxAttempts && g_sigCid == 0; attempt++) {
|
||||
g_mediaCid = 0;
|
||||
g_state = State::Idle;
|
||||
g_txLabel = 1;
|
||||
|
||||
@@ -334,6 +334,18 @@ namespace Drivers::USB::Bluetooth {
|
||||
// Enable Simple Secure Pairing
|
||||
Hci::WriteSSPMode(1);
|
||||
|
||||
// Enable Secure Connections host support (P-256 / AES-CCM). Link
|
||||
// keys are procedure-bound: a bond minted over Secure Connections
|
||||
// (BlueZ always negotiates SC -> key Type=7) CANNOT authenticate a
|
||||
// legacy link -- the controller must fail with status 5. Without
|
||||
// this, a key shared with a Linux install (dual boot, see
|
||||
// scripts/import-bluez-bond.sh) is cryptographically fine yet
|
||||
// unusable, and our own pairings mint legacy P-192 keys that Linux
|
||||
// then silently replaces. Must follow Write SSP Mode.
|
||||
uint8_t scOn = 0x01;
|
||||
Hci::SendCommand(Hci::OP_WRITE_SC_HOST_SUPPORT, &scOn, 1);
|
||||
Hci::WaitCommandComplete(Hci::OP_WRITE_SC_HOST_SUPPORT);
|
||||
|
||||
// Allow role switch + sniff on new connections. The controller default
|
||||
// link policy is 0x0000 (deny both). A multipoint headset (Bose QC
|
||||
// Ultra) that also holds a link to a phone requests a role switch to
|
||||
|
||||
@@ -184,7 +184,12 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
// the DHKey check fail (Simple Pairing Complete status 0x05). Instead the
|
||||
// handlers ENQUEUE the reply here; ProcessPendingCommands() sends it later
|
||||
// from top-level (process) context with a real, confirmed transfer.
|
||||
struct PendingHciCmd { uint16_t opcode; uint8_t len; uint8_t params[16]; };
|
||||
// params must hold the LARGEST queued reply: Link Key Request Reply is
|
||||
// 22 bytes (BD_ADDR 6 + key 16). This was 16, which silently TRUNCATED
|
||||
// the link key reply to 10 key bytes -- every stored-key reconnection
|
||||
// failed authentication (status 5) since the dawn of the driver; only
|
||||
// fresh pairings (whose replies fit) ever worked.
|
||||
struct PendingHciCmd { uint16_t opcode; uint8_t len; uint8_t params[22]; };
|
||||
static PendingHciCmd g_pending[16] = {};
|
||||
static volatile uint8_t g_pendingHead = 0;
|
||||
static volatile uint8_t g_pendingTail = 0;
|
||||
@@ -192,7 +197,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
static void EnqueueHciCmd(uint16_t opcode, const uint8_t* params, uint8_t len) {
|
||||
uint8_t next = (uint8_t)((g_pendingHead + 1) & 15);
|
||||
if (next == g_pendingTail) return; // full -> drop (should never happen)
|
||||
if (len > 16) len = 16;
|
||||
if (len > sizeof(g_pending[0].params)) len = sizeof(g_pending[0].params);
|
||||
g_pending[g_pendingHead].opcode = opcode;
|
||||
g_pending[g_pendingHead].len = len;
|
||||
for (uint8_t i = 0; i < len; i++) g_pending[g_pendingHead].params[i] = params[i];
|
||||
@@ -1271,13 +1276,23 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
while (g_pendingTail != g_pendingHead) {
|
||||
PendingHciCmd c = g_pending[g_pendingTail];
|
||||
g_pendingTail = (uint8_t)((g_pendingTail + 1) & 15);
|
||||
SendCommand(c.opcode, c.params, c.len);
|
||||
bool sent = SendCommand(c.opcode, c.params, c.len);
|
||||
// Set Connection Encryption returns Command Status (not Complete),
|
||||
// so wait on that instead of burning a 1s timeout that delays A2DP.
|
||||
if (c.opcode == OP_SET_CONN_ENCRYPT) {
|
||||
WaitCommandStatus(c.opcode, 1000);
|
||||
} else {
|
||||
WaitCommandComplete(c.opcode, nullptr, 0, 1000);
|
||||
// Log delivery + the controller's verdict: security replies
|
||||
// (link key / IO cap / confirm) failing HERE is invisible at
|
||||
// the HCI level otherwise, and looks exactly like the remote
|
||||
// rejecting authentication.
|
||||
uint8_t st[1] = { 0xEE };
|
||||
bool ok = WaitCommandComplete(c.opcode, st, 1, 1000);
|
||||
KernelLogStream(INFO, "BT-HCI") << "queued cmd " << base::hex
|
||||
<< (uint64_t)c.opcode << base::dec << " len=" << (uint64_t)c.len
|
||||
<< " sent=" << (uint64_t)(sent ? 1 : 0)
|
||||
<< " cc=" << (uint64_t)(ok ? 1 : 0)
|
||||
<< " status=" << base::hex << (uint64_t)st[0] << base::dec;
|
||||
}
|
||||
}
|
||||
s_active.store(false, std::memory_order_release);
|
||||
|
||||
@@ -49,6 +49,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
constexpr uint16_t OP_WRITE_SCAN_ENABLE = 0x0C1A;
|
||||
constexpr uint16_t OP_WRITE_CLASS_OF_DEVICE = 0x0C24;
|
||||
constexpr uint16_t OP_WRITE_SSP_MODE = 0x0C56;
|
||||
constexpr uint16_t OP_WRITE_SC_HOST_SUPPORT = 0x0C7A;
|
||||
constexpr uint16_t OP_WRITE_INQUIRY_MODE = 0x0C45;
|
||||
constexpr uint16_t OP_WRITE_PAGE_TIMEOUT = 0x0C18;
|
||||
constexpr uint16_t OP_WRITE_AUTH_ENABLE = 0x0C20;
|
||||
|
||||
@@ -83,14 +83,13 @@ with open(path, "wb") as f:
|
||||
print(f"wrote {path} ({len(blob)} bytes, 1 bond)")
|
||||
EOF
|
||||
|
||||
# Pin the configured MAC to the factory address (truthful display; the 0xFC31
|
||||
# override is cosmetic on this hardware anyway).
|
||||
# Remove any BD_ADDR override: the controller must run its factory address
|
||||
# with NO 0xFC31 write. Verified on the AX211: writing 0xFC31 -- even with
|
||||
# the factory address itself -- desyncs the address the firmware uses in SSP
|
||||
# crypto from the one on air, and every pairing fails with status 5.
|
||||
CONF="$TARGET/config/bluetooth.toml"
|
||||
mkdir -p "$TARGET/config"
|
||||
if [ -f "$CONF" ] && grep -q '^ *mac *=' "$CONF"; then
|
||||
sed -i "s/^ *mac *=.*/mac = \"$ADAPTER\"/" "$CONF"
|
||||
else
|
||||
printf 'mac = "%s"\n' "$ADAPTER" >> "$CONF"
|
||||
sed -i '/^ *mac *=/d' "$CONF"
|
||||
echo "removed mac override from $CONF (0xFC31 breaks SSP on AX211)"
|
||||
fi
|
||||
echo "set $CONF mac = $ADAPTER"
|
||||
echo "done — boot MontaukOS; the device should reconnect without re-pairing."
|
||||
|
||||
Reference in New Issue
Block a user