Почему Android снова и снова просит ввести код сопряжения: разбор ошибки BLE-адресов на ESP32
Современные телефоны скрывают свой настоящий Bluetooth-адрес, подставляя временный — и именно из-за этого гарнитура или гаджет на ESP32 может раз за разом просить заново подтвердить связь. Разбираемся, как работает механизм RPA, где прошивка допустила ошибку и как её исправили.
Если телефон при каждом подключении снова спрашивает код сопряжения — дело почти наверняка не в нём: виновата прошивка гаджета, которая запоминает устройство по временному адресу и не узнаёт его после того, как адрес меняется.
Что чувствует обычный пользователь
Сценарий знаком многим: вы подключили к телефону умную колонку, замок, датчик или любое другое Bluetooth-устройство, согласились на сопряжение — и всё работало. Потом приложение закрылось, Bluetooth выключили или вы просто отошли подальше. При следующем подключении телефон вдруг выдаёт запрос на связывание снова. Иногда — каждые пару минут, иногда — через раз.
Складывается ощущение, что «Bluetooth глючит» или что гаджет «забывает» телефон. Но если заглянуть в логи, оказывается: телефон ключи сохранил, устройство ключи сохранило, просто они не могут друг друга узнать. Причина — в том, что современные Android-смартфоны намеренно подставляют вместо своего настоящего Bluetooth-адреса временный, меняющийся примерно каждые 15 минут. Это защита от слежки. И если устройство-компаньон не умеет работать с такими адресами, оно воспринимает ваш телефон как незнакомца каждый раз, когда тот «переодевается».
Именно с такой проблемой столкнулись инженеры Krishworks Technology Innovations, отлаживая гаджет на базе ESP32 с прошивкой на NimBLE. Системный инженер Джаган С. подробно разобрал этот случай в техническом материале — и это редкая возможность посмотреть, как одна неточность в коде превращается в раздражающий баг у миллионов пользователей.
Как выглядела проблема
Первое подключение и сопряжение проходили всегда без сучка и задоринки. Проблема начиналась со второго подключения и далее. Цикл простой: телефон находит устройство, подключается, пользователь подтверждает связывание, затем связь обрывается — приложение закрыто, Bluetooth выключен, устройство вне зоны действия. При повторном подключении всё должно работать «в один клик», без вопросов.
Фактически же связь обрывалась с кодом CONNECTION_TERMINATED_BY_LOCAL_HOST (HCI-код 0x16), а затем Android снова просил выполнить сопряжение. Показательно, что Samsung Galaxy S25 и Google Pixel воспроизводили это каждый раз, тогда как на старых Android-устройствах ошибка проявлялась далеко не сразу.
Код 0x16 сам по себе подсказка: разрыв инициирует не радиомодуль и не помехи, а программный стек на стороне телефона. Обычно это означает, что ключи шифрования не сошлись — то есть телефон считает связь незащищённой и предпочитает разорвать её, чем продолжать небезопасный обмен.
Бондинг: почему устройства должны «помнить» друг друга
Чтобы понять поломку, нужно разобраться, чем сопряжение отличается от связывания. Сопряжение (pairing) — это разовый обмен ключами для текущей сессии. Бондинг (bonding) — это то же самое, но с сохранением: обе стороны пишут обменянные ключи в постоянную память, чтобы при следующем подключении зашифровать канал сразу, без вопросов к пользователю. На телефоне это список «Связанные устройства» в настройках Bluetooth, на ESP32 — хранилище NVS внутри прошивки.
Сам процесс идёт в три шага. Сначала устройства обмениваются возможностями: есть ли экран, клавиатура, хотят ли они сохранить связь, поддерживают ли современный стандарт LE Secure Connections. От этого зависит метод: просто «подтвердите», введите цифры с экрана или сравните коды на двух дисплеях. Далее идёт генерация ключей — по сути математический обмен, в результате которого обе стороны получают общий долговременный ключ LTK. Наконец, по уже зашифрованному каналу стороны делятся дополнительными ключами — среди них IRK (ключ разрешения личности) вместе с постоянным адресом устройства.
Каждый из этих ключей решает свою задачу. LTK шифрует канал и позволяет не спрашивать код повторно. Постоянный адрес — это «имя» устройства, под которым вся связка ключей кладётся в память. А IRK — ключ, который позволяет распознать устройство, даже когда оно приходит с другим, временным адресом. Потеря любого звена — и связывание рассыпается.
Зачем телефон прячет свой адрес
Здесь кроется главный сдвиг последних лет. Постоянный Bluetooth-адрес — это фактически маячок. Сканер в магазине, на вокзале или в офисе может сутками логировать один и тот же адрес и отслеживать перемещения человека. Решение пришло в Bluetooth 4.2 в виде режима BLE Privacy: телефон вместо постоянного адреса транслирует Resolvable Private Address (RPA) — случайно выглядящие 48 бит, которые меняются примерно каждые 15 минут и при каждом новом подключении.
Устроено это изящно. Половину адреса составляет случайная часть, вторая — 24-битный хэш, вычисленный из секретного IRK. Устройство, у которого есть этот IRK, пересчитывает хэш и понимает: «а, это тот самый телефон». Устройство без IRK видит каждый раз новый незнакомый адрес. По такой же схеме работают и другие типы адресов: публичный — постоянный и отслеживаемый всеми, статический случайный — меняется только при перезагрузке, неразрешимый частный — меняется часто и не распознаётся никем.
Ключевой момент: чтобы связать временный адрес с настоящим, прошивка должна получить IRK во время сопряжения. Именно этого не происходило.
Что показали логи
Отладку вели с обеих сторон: логи приложения на телефоне и логи прошивки. В мобильном приложении и в утилите nRF Connect фиксировали каждый цикл «связать — отключить — подключить», записывали код разрыва и проверяли, что телефон считает себя связанным — и считал. При этом адрес, который ESP32 видела на входящем подключении, менялся от цикла к циклу, что подтвердало: телефон использует RPA.
На стороне прошивки логировали все события стека — подключение, смену шифрования, разрыв, повторное сопряжение, — печатали оба адреса собеседника: тот, что прилетел «по воздуху» (peer_ota_addr), и тот, что стек считает постоянным (peer_id_addr). Плюс дамп хранилища связанных устройств после каждого сопряжения.
Расхождение между ожидаемым и реальным было разительным. По воздуху приходил RPA, и он не резолвился в постоянный адрес. В NVS связка ключей сохранялась под временным адресом. При повторном подключении приходил уже другой RPA — соответствующей записи не находилось, ключа шифрования не оказывалось, телефон воспринимал это как сломанную связь, разрывал канал с кодом 0x16 и начинал сопряжение заново. Причём каждое новое сопряжение добавляло в память ещё одну запись под ещё одним временным адресом — хранилище постепенно забивалось мусором.
Корень проблемы: память по временному адресу
Причина оказалась ровно там, куда указывали симптомы. В прошивке ESP32 на NimBLE обмен идентификационными ключами не был включён: во время сопряжения устройства не запрашивали и не передавали IRK с постоянным адресом. Без IRK стек физически не мог сопоставить временный адрес телефона с его настоящей личностью, и «идентичностью» собеседника становился тот самый временный RPA. Под него и сохранялась вся связка ключей.
Получалась замкнутая цепочка: телефон меняет адрес, устройство ищет запись по новому адресу, не находит, отвечает, что ключа нет; телефон обрывает связь и предлагает сопряжение заново. Старые Android-смартфоны держали адрес дольше и часто не успевали его сменить между отключением и переподключением — баг оставался скрытым. Новые устройства, такие как Galaxy S25 и Pixel, меняют адрес агрессивнее, и ошибка проявилась во всей красе.
Важный вывод: это была не поломка Android и не «капризы» Bluetooth. Это была ошибка приватности в прошивке — устройство использовало временный адрес как ключ для хранения данных.
Как это исправили
Решение состояло из четырёх частей, и все они логично следуют одна из другой.
Во-первых, связки ключей стали сохранять в постоянную память так, чтобы они переживали перезагрузку. Во-вторых, включили обмен идентификационными ключами: теперь при сопряжении телефон передаёт свой IRK и постоянный адрес. В-третьих, поиск и сохранение связки перевели на постоянный адрес собеседника — временный RPA для этого больше не используется ни при каких условиях. Если телефон удалил связь и подключается заново, старая запись под этим постоянным адресом сначала удаляется, чтобы не копить хлам. В-четвёртых, полученный IRK добавляют в так называемый resolving list — список разрешения адресов — и восстанавливают его из памяти при старте, чтобы устройство узнавало телефон даже после выключения питания.
Теперь цикл выглядит иначе: ключ IRK приезжает один раз при сопряжении, сохраняется вместе с долговременным ключом под постоянным адресом, а любой временный адрес, с которым телефон приходит потом, распознаётся и приводит к той же записи.
Проверка
После правки повторили исходный цикл и добавили сценарии, которые принудительно меняют адрес: переподключение сразу, переподключение после ожидания больше 15 минут, когда RPA гарантированно обновился, выключение и включение Bluetooth на телефоне, перезагрузка ESP32 и случай, когда пользователь удалил связь только на телефоне.
Во всех случаях устройство подключалось на сохранённом ключе без запроса на сопряжение и без обрыва с кодом 0x16. В логах при этом видно ожидаемое поведение: временный адрес меняется от подключения к подключению, постоянный остаётся прежним, а в хранилище — ровно одна запись на телефон, сохранённая по постоянному адресу.
Что стоит запомнить
Повторные запросы на связывание — это почти всегда история про приватность, а не про сломанный Bluetooth. Современные телефоны и на iOS, и на Android обязаны скрывать свой настоящий адрес, и прошивкам нельзя использовать адрес «по воздуху» как ключ для хранения данных. Обмен идентификационными ключами должен быть включён всегда, иначе распознавание невозможно в принципе.
Для разработчиков полезны и более приземлённые уроки. Печатайте в логах оба адреса — временный и распознанный постоянный; с их сравнением такие баги видно за минуты, а не за недели. Обращайте внимание на код разрыва: 0x16 после переподключения почти всегда означает несовпадение ключей, а не проблему с антенной или помехами. И включайте в тест-план ожидание дольше таймаута смены адреса и перезагрузку устройства — а тестировать лучше на самых свежих смартфонах, потому что именно они проявляют эти ошибки первыми.
Для обычного пользователя вывод простой: если гаджет раз за разом забывает ваш телефон, это повод написать разработчикам прошивки — а не менять смартфон. Хорошая новость в том, что лечится это одной аккуратной правкой на уровне хранения ключей, после которой связь наконец работает так, как и обещают: связали один раз — и больше не вспоминаете.