agent: fix NoInputNoOutput agents rejecting all pairing requests - #190
agent: fix NoInputNoOutput agents rejecting all pairing requests#190abezukor wants to merge 1 commit into
Conversation
The Agent documentation states:
Setting all handlers to [None] (the default) will result in a
`NoInputNoOutput` handler that accepts all requests.
This did not work. An all-None Agent published the NoInputNoOutput
capability, but rejected the RequestAuthorization call that BlueZ
makes for incoming just-works pairing, so every pairing attempt
failed. This change makes the code match the documented behavior.
It was also impossible to fix from user code: setting
request_authorization to accept the pairing changed the published
capability to DisplayYesNo, causing MITM-capable peers (e.g. iOS)
to negotiate numeric comparison instead of just-works.
Fix both issues by:
- Excluding request_authorization and authorize_service from the
capability derivation, since both are local authorization policy
decisions that BlueZ invokes regardless of the published input
and output capabilities. Only request_confirmation now implies
yes/no capability.
- Installing default RequestAuthorization and AuthorizeService
handlers that accept all requests when no handlers are set, so
an all-None Agent behaves as documented.
Note for existing code: an agent with only request_authorization or
authorize_service set now publishes NoInputNoOutput instead of
DisplayYesNo, and an all-None agent now accepts incoming just-works
pairing instead of rejecting it, as the documentation always stated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
I just encountered this too. As far as I understand the fix looks reasonable. Anything I can do to help get this merged? Below is a summary from Opus: I hit this without knowing the PR existed and arrived at the same two conclusions, so here is the wire-level evidence in case it is useful. Setup. bluer 0.17.4, BlueZ 5.87 on both ends, Fedora 44 (kernels 7.1.3 / 7.1.5). The peripheral registered exactly the agent the docs describe: Agent { request_default: true, ..Default::default() } The intent was the documented one: NoInputNoOutput, just-works, nothing to answer on a headless box. Every LE pairing attempt failed, with the host reporting only org.bluez.Error.AuthenticationFailed. btmon on the peripheral:
The capability negotiation is correct — NoInputNoOutput selects just-works, and the kernel says so with Confirm hint: 0x01. What goes wrong is downstream: bluetoothd turns that into RequestAuthorization, Agent::call finds the handler unset and answers ReqError::Rejected, and that becomes a User Confirmation Negative Reply. So the peripheral rejects its own pairing, and the failure surfaces as Numeric comparison failed (0x0c) on a link where no numeric comparison was ever meant to happen. That reason code sends you looking at association models, the host, or the controller — the one place it does not point is a missing callback in your own agent. Worth stressing as a footgun rather than only a doc bug: an all-None agent is the natural thing to write for a headless just-works device, it is what the docs recommend, and it is the one configuration that cannot pair at all. |
Problem
It is currently impossible to register a working
NoInputNoOutputagent, because the capability string passed toRegisterAgentis derived automatically from which handler fields are set:None→ capability isNoInputNoOutput, but BlueZ still callsRequestAuthorizationfor incoming just-works pairing (andAuthorizeServicefor service connections). Since the handler isNone, bluer replies withorg.bluez.Error.Rejected— so every pairing attempt fails.request_authorizationto accept → the derivation flips the capability toDisplayYesNo, so MITM-capable centrals (iOS in particular sets the MITM flag) negotiate numeric comparison instead of just-works. The phone then shows a "confirm this code" dialog for a code that a headless device cannot display anywhere.This also contradicts the existing
Agentdocumentation, which states:The all-
Noneagent rejected all requests instead, so the documented behavior never worked.Use case
A headless device (no display, no input) exposing a GATT service with
encrypt-read/encrypt-writecharacteristics. Phones must be able to pair via just-works with a single consent tap and no passkey dialog. This requires advertisingNoInputNoOutputand acceptingRequestAuthorization/AuthorizeService.Fix
Exclude
request_authorizationandauthorize_servicefrom the capability derivation. Per the BlueZ agent API,RequestAuthorizationis called "to request the user to authorize an incoming pairing attempt which would in other circumstances trigger the just-works model" — it is a local authorization policy decision, invoked regardless of the published capability, not an input/output capability. The same applies toAuthorizeService. Onlyrequest_confirmationnow implies yes/no capability. With this change, an agent with only authorization handlers correctly advertisesNoInputNoOutput.Make the all-
Nonedefault match the documented behavior. When no handlers are set, defaultRequestAuthorizationandAuthorizeServicehandlers that accept all requests are installed at registration, so the defaultAgentis the documented "NoInputNoOutputhandler that accepts all requests". Code-input requests (RequestPinCode,RequestPasskey, etc.) still reject — BlueZ does not send them to aNoInputNoOutputagent.Behavior changes
request_authorization/authorize_serviceset now publishesNoInputNoOutputinstead ofDisplayYesNo. Such configurations were effectively broken before, since they had norequest_confirmationto satisfy numeric comparison.Noneagent now accepts incoming just-works pairing instead of rejecting it, matching the existing documentation.cargo buildandcargo clippy --all-featurespass with no new warnings.🤖 Generated with Claude Code - I (a Human) did manually verify the issue and audit the code.