| // Copyright 2017 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "peer_cache.h" |
| |
| #include <lib/fit/function.h> |
| #include <zircon/assert.h> |
| |
| #include "lib/async/default.h" |
| #include "src/connectivity/bluetooth/core/bt-host/common/random.h" |
| #include "src/connectivity/bluetooth/core/bt-host/gap/peer.h" |
| #include "src/connectivity/bluetooth/core/bt-host/hci/connection.h" |
| #include "src/connectivity/bluetooth/core/bt-host/hci/low_energy_scanner.h" |
| #include "src/connectivity/bluetooth/core/bt-host/sm/types.h" |
| |
| namespace bt::gap { |
| |
| namespace { |
| |
| // Return an address with the same value as given, but with type kBREDR for |
| // kLEPublic addresses and vice versa. |
| DeviceAddress GetAliasAddress(const DeviceAddress& address) { |
| if (address.type() == DeviceAddress::Type::kBREDR) { |
| return {DeviceAddress::Type::kLEPublic, address.value()}; |
| } else if (address.type() == DeviceAddress::Type::kLEPublic) { |
| return {DeviceAddress::Type::kBREDR, address.value()}; |
| } |
| return address; |
| } |
| |
| } // namespace |
| |
| Peer* PeerCache::NewPeer(const DeviceAddress& address, bool connectable) { |
| ZX_DEBUG_ASSERT(thread_checker_.is_thread_valid()); |
| auto* const peer = InsertPeerRecord(RandomPeerId(), address, connectable); |
| if (peer) { |
| UpdateExpiry(*peer); |
| NotifyPeerUpdated(*peer, Peer::NotifyListenersChange::kBondNotUpdated); |
| } |
| return peer; |
| } |
| |
| void PeerCache::ForEach(PeerCallback f) { |
| ZX_DEBUG_ASSERT(thread_checker_.is_thread_valid()); |
| ZX_DEBUG_ASSERT(f); |
| for (const auto& iter : peers_) { |
| f(*iter.second.peer()); |
| } |
| } |
| |
| bool PeerCache::AddBondedPeer(BondingData bd) { |
| ZX_DEBUG_ASSERT(thread_checker_.is_thread_valid()); |
| ZX_DEBUG_ASSERT(bd.address.type() != DeviceAddress::Type::kLEAnonymous); |
| |
| const bool bond_le = |
| bd.le_pairing_data.peer_ltk || bd.le_pairing_data.local_ltk || bd.le_pairing_data.csrk; |
| const bool bond_bredr = bd.bredr_link_key.has_value(); |
| |
| // |bd.le_pairing_data| must contain either a LTK or CSRK for LE Security Mode 1 or 2. |
| // |
| // TODO(fxbug.dev/2761): the address type checks here don't add much value because the address |
| // type is derived from the presence of FIDL bredr_bond and le_bond fields, so the check really |
| // should be whether at least one of the mandatory bond secrets is present. |
| if (bd.address.IsLowEnergy() && !bond_le) { |
| bt_log(ERROR, "gap-le", "mandatory keys missing: no LTK or CSRK (id: %s)", |
| bt_str(bd.identifier)); |
| return false; |
| } |
| |
| if (bd.address.IsBrEdr() && !bond_bredr) { |
| bt_log(ERROR, "gap-bredr", "mandatory link key missing (id: %s)", bt_str(bd.identifier)); |
| return false; |
| } |
| |
| auto* peer = InsertPeerRecord(bd.identifier, bd.address, /*connectable=*/true); |
| if (!peer) { |
| return false; |
| } |
| |
| // A bonded peer must have its identity known. |
| peer->set_identity_known(true); |
| |
| if (bd.name.has_value()) { |
| peer->SetName(bd.name.value()); |
| } |
| |
| if (bond_le) { |
| ZX_ASSERT(bd.le_pairing_data.irk.has_value() == |
| bd.le_pairing_data.identity_address.has_value()); |
| peer->MutLe().SetBondData(bd.le_pairing_data); |
| ZX_ASSERT(peer->le()->bonded()); |
| |
| // Add the peer to the resolving list if it has an IRK. |
| if (bd.le_pairing_data.irk) { |
| le_resolving_list_.Add(bd.le_pairing_data.identity_address.value(), |
| bd.le_pairing_data.irk.value().value()); |
| } |
| } |
| |
| if (bond_bredr) { |
| for (auto& service : bd.bredr_services) { |
| peer->MutBrEdr().AddService(std::move(service)); |
| } |
| |
| peer->MutBrEdr().SetBondData(*bd.bredr_link_key); |
| ZX_DEBUG_ASSERT(peer->bredr()->bonded()); |
| } |
| |
| if (peer->technology() == TechnologyType::kDualMode) { |
| address_map_[GetAliasAddress(bd.address)] = bd.identifier; |
| } |
| |
| ZX_DEBUG_ASSERT(!peer->temporary()); |
| ZX_DEBUG_ASSERT(peer->bonded()); |
| bt_log(TRACE, "gap", "restored bonded peer: %s, id: %s", bt_str(bd.address), |
| bt_str(bd.identifier)); |
| |
| // Don't call UpdateExpiry(). Since a bonded peer starts out as |
| // non-temporary it is not necessary to ever set up the expiration callback. |
| NotifyPeerUpdated(*peer, Peer::NotifyListenersChange::kBondNotUpdated); |
| return true; |
| } |
| |
| bool PeerCache::StoreLowEnergyBond(PeerId identifier, const sm::PairingData& bond_data) { |
| ZX_ASSERT(bond_data.irk.has_value() == bond_data.identity_address.has_value()); |
| |
| auto log_bond_failure = fit::defer([this] { peer_metrics_.LogLeBondFailureEvent(); }); |
| |
| auto* peer = FindById(identifier); |
| if (!peer) { |
| bt_log(WARN, "gap-le", "failed to store bond for unknown peer (peer: %s)", bt_str(identifier)); |
| return false; |
| } |
| |
| // Either a LTK or CSRK is mandatory for bonding (the former is needed for LE |
| // Security Mode 1 and the latter is needed for Mode 2). |
| if (!bond_data.peer_ltk && !bond_data.local_ltk && !bond_data.csrk) { |
| bt_log(WARN, "gap-le", "mandatory keys missing: no LTK or CSRK (peer: %s)", bt_str(identifier)); |
| return false; |
| } |
| |
| if (bond_data.identity_address) { |
| auto existing_id = FindIdByAddress(*bond_data.identity_address); |
| if (!existing_id) { |
| // Map the new address to |peer|. We leave old addresses that map to |
| // this peer in the cache in case there are any pending controller |
| // procedures that expect them. |
| // TODO(armansito): Maybe expire the old address after a while? |
| address_map_[*bond_data.identity_address] = identifier; |
| } else if (*existing_id != identifier) { |
| bt_log(WARN, "gap-le", "identity address %s for peer %s belongs to another peer %s!", |
| bt_str(*bond_data.identity_address), bt_str(identifier), bt_str(*existing_id)); |
| return false; |
| } |
| // We have either created a new mapping or the identity address already |
| // maps to this peer. |
| } |
| |
| // TODO(fxbug.dev/1212): Check that we're not downgrading the security level before |
| // overwriting the bond. |
| peer->MutLe().SetBondData(bond_data); |
| ZX_DEBUG_ASSERT(!peer->temporary()); |
| ZX_DEBUG_ASSERT(peer->le()->bonded()); |
| |
| // Add the peer to the resolving list if it has an IRK. |
| if (peer->identity_known() && bond_data.irk) { |
| le_resolving_list_.Add(*bond_data.identity_address, bond_data.irk->value()); |
| } |
| |
| if (bond_data.cross_transport_key) { |
| peer->StoreBrEdrCrossTransportKey(*bond_data.cross_transport_key); |
| } |
| |
| // Report the bond for persisting only if the identity of the peer is known. |
| if (peer->identity_known()) { |
| NotifyPeerBonded(*peer); |
| } |
| |
| log_bond_failure.cancel(); |
| peer_metrics_.LogLeBondSuccessEvent(); |
| return true; |
| } |
| |
| bool PeerCache::StoreBrEdrBond(const DeviceAddress& address, const sm::LTK& link_key) { |
| ZX_DEBUG_ASSERT(thread_checker_.is_thread_valid()); |
| ZX_DEBUG_ASSERT(address.type() == DeviceAddress::Type::kBREDR); |
| auto* peer = FindByAddress(address); |
| if (!peer) { |
| bt_log(WARN, "gap-bredr", "failed to store bond for unknown peer (address: %s)", |
| bt_str(address)); |
| return false; |
| } |
| |
| // TODO(fxbug.dev/1212): Check that we're not downgrading the security level before |
| // overwriting the bond. |
| peer->MutBrEdr().SetBondData(link_key); |
| ZX_DEBUG_ASSERT(!peer->temporary()); |
| ZX_DEBUG_ASSERT(peer->bredr()->bonded()); |
| |
| NotifyPeerBonded(*peer); |
| return true; |
| } |
| |
| bool PeerCache::SetAutoConnectBehaviorForIntentionalDisconnect(PeerId peer_id) { |
| Peer* const peer = FindById(peer_id); |
| if (!peer) { |
| bt_log(WARN, "gap-le", |
| "failed to update auto-connect behavior to kSkipUntilNextConnection for " |
| "unknown peer: %s", |
| bt_str(peer_id)); |
| return false; |
| } |
| |
| bt_log(DEBUG, "gap-le", "updated auto-connect behavior to kSkipUntilNextConnection (peer: %s)", |
| bt_str(peer_id)); |
| |
| peer->MutLe().set_auto_connect_behavior(Peer::AutoConnectBehavior::kSkipUntilNextConnection); |
| |
| // TODO(fxbug.dev/37584): When implementing auto-connect behavior tracking for classic bluetooth, |
| // consider tracking this policy for the peer as a whole unless we think this policy should be |
| // applied separately for each transport (per armansito@). |
| |
| return true; |
| } |
| |
| bool PeerCache::SetAutoConnectBehaviorForSuccessfulConnection(PeerId peer_id) { |
| Peer* const peer = FindById(peer_id); |
| if (!peer) { |
| bt_log(WARN, "gap-le", "failed to update auto-connect behavior to kAlways for unknown peer: %s", |
| bt_str(peer_id)); |
| return false; |
| } |
| |
| bt_log(DEBUG, "gap-le", "updated auto-connect behavior to kAlways (peer: %s)", bt_str(peer_id)); |
| |
| peer->MutLe().set_auto_connect_behavior(Peer::AutoConnectBehavior::kAlways); |
| |
| // TODO(fxbug.dev/37584): Implement auto-connect behavior tracking for classic bluetooth. |
| |
| return true; |
| } |
| |
| bool PeerCache::RemoveDisconnectedPeer(PeerId peer_id) { |
| Peer* const peer = FindById(peer_id); |
| if (!peer) { |
| return true; |
| } |
| |
| if (peer->connected()) { |
| return false; |
| } |
| |
| RemovePeer(peer); |
| return true; |
| } |
| |
| Peer* PeerCache::FindById(PeerId peer_id) const { |
| ZX_DEBUG_ASSERT(thread_checker_.is_thread_valid()); |
| auto iter = peers_.find(peer_id); |
| return iter != peers_.end() ? iter->second.peer() : nullptr; |
| } |
| |
| Peer* PeerCache::FindByAddress(const DeviceAddress& in_address) const { |
| ZX_DEBUG_ASSERT(thread_checker_.is_thread_valid()); |
| |
| std::optional<DeviceAddress> address; |
| if (in_address.IsResolvablePrivate()) { |
| address = le_resolving_list_.Resolve(in_address); |
| } |
| |
| // Fall back to the input if an identity wasn't resolved. |
| if (!address) { |
| address = in_address; |
| } |
| |
| ZX_DEBUG_ASSERT(address); |
| auto identifier = FindIdByAddress(*address); |
| if (!identifier) { |
| return nullptr; |
| } |
| |
| auto* p = FindById(*identifier); |
| ZX_DEBUG_ASSERT(p); |
| return p; |
| } |
| |
| void PeerCache::AttachInspect(inspect::Node& parent, std::string name) { |
| node_ = parent.CreateChild(name); |
| |
| if (!node_) { |
| return; |
| } |
| |
| peer_metrics_.AttachInspect(node_); |
| |
| for (auto& [_, record] : peers_) { |
| record.peer()->AttachInspect(node_, node_.UniqueName("peer_")); |
| } |
| } |
| |
| PeerCache::CallbackId PeerCache::add_peer_updated_callback(PeerCallback callback) { |
| auto [iter, success] = peer_updated_callbacks_.emplace(next_callback_id_++, std::move(callback)); |
| ZX_ASSERT(success); |
| return iter->first; |
| } |
| |
| bool PeerCache::remove_peer_updated_callback(CallbackId id) { |
| return peer_updated_callbacks_.erase(id); |
| } |
| |
| // Private methods below. |
| |
| Peer* PeerCache::InsertPeerRecord(PeerId identifier, const DeviceAddress& address, |
| bool connectable) { |
| if (FindIdByAddress(address)) { |
| bt_log(WARN, "gap", "tried to insert peer with existing address: %s", |
| address.ToString().c_str()); |
| return nullptr; |
| } |
| |
| auto store_le_bond_cb = [this, identifier](const sm::PairingData& data) { |
| return StoreLowEnergyBond(identifier, data); |
| }; |
| |
| std::unique_ptr<Peer> peer(new Peer(fit::bind_member<&PeerCache::NotifyPeerUpdated>(this), |
| fit::bind_member<&PeerCache::UpdateExpiry>(this), |
| fit::bind_member<&PeerCache::MakeDualMode>(this), |
| std::move(store_le_bond_cb), identifier, address, connectable, |
| &peer_metrics_)); |
| if (node_) { |
| peer->AttachInspect(node_, node_.UniqueName("peer_")); |
| } |
| |
| // Note: we must construct the PeerRecord in-place, because it doesn't |
| // support copy or move. |
| auto [iter, inserted] = peers_.try_emplace(peer->identifier(), std::move(peer), |
| [this, p = peer.get()] { RemovePeer(p); }); |
| if (!inserted) { |
| bt_log(WARN, "gap", "tried to insert peer with existing ID: %s", bt_str(identifier)); |
| return nullptr; |
| } |
| |
| address_map_[address] = identifier; |
| return iter->second.peer(); |
| } |
| |
| void PeerCache::NotifyPeerBonded(const Peer& peer) { |
| ZX_DEBUG_ASSERT(peers_.find(peer.identifier()) != peers_.end()); |
| ZX_DEBUG_ASSERT(peers_.at(peer.identifier()).peer() == &peer); |
| ZX_DEBUG_ASSERT_MSG(peer.identity_known(), "peers not allowed to bond with unknown identity!"); |
| |
| bt_log(INFO, "gap", "successfully bonded (peer: %s)", bt_str(peer)); |
| if (peer_bonded_callback_) { |
| peer_bonded_callback_(peer); |
| } |
| } |
| |
| void PeerCache::NotifyPeerUpdated(const Peer& peer, Peer::NotifyListenersChange change) { |
| ZX_DEBUG_ASSERT(peers_.find(peer.identifier()) != peers_.end()); |
| ZX_DEBUG_ASSERT(peers_.at(peer.identifier()).peer() == &peer); |
| |
| for (auto& [_, peer_updated_callback] : peer_updated_callbacks_) { |
| peer_updated_callback(peer); |
| } |
| |
| if (change == Peer::NotifyListenersChange::kBondUpdated) { |
| ZX_ASSERT(peer.bonded()); |
| bt_log(INFO, "gap", "peer bond updated %s", bt_str(peer)); |
| if (peer_bonded_callback_) { |
| peer_bonded_callback_(peer); |
| } |
| } |
| } |
| |
| void PeerCache::UpdateExpiry(const Peer& peer) { |
| auto peer_record_iter = peers_.find(peer.identifier()); |
| ZX_DEBUG_ASSERT(peer_record_iter != peers_.end()); |
| |
| auto& peer_record = peer_record_iter->second; |
| ZX_DEBUG_ASSERT(peer_record.peer() == &peer); |
| |
| const auto cancel_res = peer_record.removal_task()->Cancel(); |
| ZX_DEBUG_ASSERT(cancel_res == ZX_OK || cancel_res == ZX_ERR_NOT_FOUND); |
| |
| // Previous expiry task has been canceled. Re-schedule only if the peer is |
| // temporary. |
| if (peer.temporary()) { |
| const auto schedule_res = |
| peer_record.removal_task()->PostDelayed(async_get_default_dispatcher(), kCacheTimeout); |
| ZX_DEBUG_ASSERT(schedule_res == ZX_OK || schedule_res == ZX_ERR_BAD_STATE); |
| } |
| } |
| |
| void PeerCache::MakeDualMode(const Peer& peer) { |
| ZX_ASSERT(address_map_.at(peer.address()) == peer.identifier()); |
| const auto address_alias = GetAliasAddress(peer.address()); |
| auto [iter, inserted] = address_map_.try_emplace(address_alias, peer.identifier()); |
| ZX_ASSERT_MSG(inserted || iter->second == peer.identifier(), |
| "%s can't become dual-mode because %s maps to %s", bt_str(peer.identifier()), |
| bt_str(address_alias), bt_str(iter->second)); |
| bt_log(INFO, "gap", "peer became dual mode (peer: %s, address: %s, alias: %s)", |
| bt_str(peer.identifier()), bt_str(peer.address()), bt_str(address_alias)); |
| |
| // The peer became dual mode in lieu of adding a new peer but is as |
| // significant, so notify listeners of the change. |
| NotifyPeerUpdated(peer, Peer::NotifyListenersChange::kBondNotUpdated); |
| } |
| |
| void PeerCache::RemovePeer(Peer* peer) { |
| ZX_DEBUG_ASSERT(peer); |
| |
| auto peer_record_it = peers_.find(peer->identifier()); |
| ZX_DEBUG_ASSERT(peer_record_it != peers_.end()); |
| ZX_DEBUG_ASSERT(peer_record_it->second.peer() == peer); |
| |
| PeerId id = peer->identifier(); |
| bt_log(DEBUG, "gap", "removing peer %s", bt_str(id)); |
| for (auto iter = address_map_.begin(); iter != address_map_.end();) { |
| if (iter->second == id) { |
| iter = address_map_.erase(iter); |
| } else { |
| iter++; |
| } |
| } |
| |
| if (peer->le() && peer->le()->bonded()) { |
| if (auto& address = peer->le()->bond_data()->identity_address) { |
| le_resolving_list_.Remove(*address); |
| } |
| } |
| |
| peers_.erase(peer_record_it); // Destroys |peer|. |
| if (peer_removed_callback_) { |
| peer_removed_callback_(id); |
| } |
| } |
| |
| std::optional<PeerId> PeerCache::FindIdByAddress(const DeviceAddress& address) const { |
| auto iter = address_map_.find(address); |
| if (iter == address_map_.end()) { |
| // Search again using the other technology's address. This is necessary when |
| // a dual-mode peer is known by only one technology and is then discovered |
| // or connected on its other technology. |
| iter = address_map_.find(GetAliasAddress(address)); |
| } |
| |
| if (iter == address_map_.end()) { |
| return {}; |
| } |
| return {iter->second}; |
| } |
| |
| } // namespace bt::gap |