| // Copyright 2024 The Pigweed Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| // use this file except in compliance with the License. You may obtain a copy of |
| // the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| // License for the specific language governing permissions and limitations under |
| // the License. |
| |
| #include "pw_bluetooth_proxy/internal/acl_data_channel.h" |
| |
| #include <cstdint> |
| #include <mutex> |
| #include <optional> |
| |
| #include "lib/stdcompat/utility.h" |
| #include "pw_assert/check.h" |
| #include "pw_bluetooth/emboss_util.h" |
| #include "pw_bluetooth/hci_data.emb.h" |
| #include "pw_bluetooth_proxy/internal/l2cap_channel_manager.h" |
| #include "pw_bluetooth_proxy/internal/recombiner.h" |
| #include "pw_containers/algorithm.h" // IWYU pragma: keep |
| #include "pw_log/log.h" |
| #include "pw_span/cast.h" |
| #include "pw_status/status.h" |
| |
| namespace pw::bluetooth::proxy { |
| |
| AclDataChannel::AclConnection::AclConnection( |
| AclTransportType transport, |
| uint16_t connection_handle, |
| uint16_t num_pending_packets, |
| L2capChannelManager& l2cap_channel_manager) |
| : transport_(transport), |
| connection_handle_(connection_handle), |
| num_pending_packets_(num_pending_packets), |
| leu_signaling_channel_(l2cap_channel_manager, connection_handle), |
| aclu_signaling_channel_(l2cap_channel_manager, connection_handle) { |
| PW_LOG_INFO( |
| "btproxy: AclConnection ctor. transport_: %u, connection_handle_: %#x", |
| cpp23::to_underlying(transport_), |
| connection_handle_); |
| } |
| |
| AclDataChannel::SendCredit::SendCredit(SendCredit&& other) { |
| *this = std::move(other); |
| } |
| |
| AclDataChannel::SendCredit& AclDataChannel::SendCredit::operator=( |
| SendCredit&& other) { |
| if (this != &other) { |
| transport_ = other.transport_; |
| relinquish_fn_ = std::move(other.relinquish_fn_); |
| other.relinquish_fn_ = nullptr; |
| } |
| return *this; |
| } |
| |
| AclDataChannel::SendCredit::~SendCredit() { |
| if (relinquish_fn_) { |
| relinquish_fn_(transport_); |
| } |
| } |
| |
| AclDataChannel::SendCredit::SendCredit( |
| AclTransportType transport, |
| Function<void(AclTransportType transport)>&& relinquish_fn) |
| : transport_(transport), relinquish_fn_(std::move(relinquish_fn)) {} |
| |
| void AclDataChannel::SendCredit::MarkUsed() { |
| PW_CHECK(relinquish_fn_); |
| relinquish_fn_ = nullptr; |
| } |
| |
| void AclDataChannel::Reset() { |
| { |
| std::lock_guard lock(credit_mutex_); |
| // Reset credits first so no packets queued in signaling channels can be |
| // sent. |
| le_credits_.Reset(); |
| br_edr_credits_.Reset(); |
| } |
| { |
| std::lock_guard lock(connection_mutex_); |
| acl_connections_.clear(); |
| } |
| } |
| |
| void AclDataChannel::Credits::Reset() { |
| proxy_max_ = 0; |
| proxy_pending_ = 0; |
| } |
| |
| uint16_t AclDataChannel::Credits::Reserve(uint16_t controller_max) { |
| PW_CHECK(!Initialized(), |
| "AclDataChannel is already initialized. Proxy should have been " |
| "reset before this."); |
| |
| proxy_max_ = std::min(controller_max, to_reserve_); |
| const uint16_t host_max = controller_max - proxy_max_; |
| |
| PW_LOG_INFO( |
| "Bluetooth Proxy reserved %d ACL data credits. Passed %d on to host.", |
| proxy_max_, |
| host_max); |
| |
| if (proxy_max_ < to_reserve_) { |
| PW_LOG_ERROR( |
| "Only was able to reserve %d acl data credits rather than the " |
| "configured %d from the controller provided's data credits of %d. ", |
| proxy_max_, |
| to_reserve_, |
| controller_max); |
| } |
| |
| return host_max; |
| } |
| |
| Status AclDataChannel::Credits::MarkPending(uint16_t num_credits) { |
| if (num_credits > Available()) { |
| return Status::ResourceExhausted(); |
| } |
| |
| proxy_pending_ += num_credits; |
| |
| return OkStatus(); |
| } |
| |
| void AclDataChannel::Credits::MarkCompleted(uint16_t num_credits) { |
| if (num_credits > proxy_pending_) { |
| PW_LOG_ERROR("Tried to mark completed more packets than were pending."); |
| proxy_pending_ = 0; |
| } else { |
| proxy_pending_ -= num_credits; |
| } |
| } |
| |
| AclDataChannel::Credits& AclDataChannel::LookupCredits( |
| AclTransportType transport) { |
| switch (transport) { |
| case AclTransportType::kBrEdr: |
| return br_edr_credits_; |
| case AclTransportType::kLe: |
| return le_credits_; |
| default: |
| PW_CHECK(false, "Invalid transport type"); |
| } |
| } |
| |
| const AclDataChannel::Credits& AclDataChannel::LookupCredits( |
| AclTransportType transport) const { |
| switch (transport) { |
| case AclTransportType::kBrEdr: |
| return br_edr_credits_; |
| case AclTransportType::kLe: |
| return le_credits_; |
| default: |
| PW_CHECK(false, "Invalid transport type"); |
| } |
| } |
| |
| void AclDataChannel::ProcessReadBufferSizeCommandCompleteEvent( |
| emboss::ReadBufferSizeCommandCompleteEventWriter read_buffer_event) { |
| { |
| std::lock_guard lock(credit_mutex_); |
| const uint16_t controller_max = |
| read_buffer_event.total_num_acl_data_packets().Read(); |
| const uint16_t host_max = br_edr_credits_.Reserve(controller_max); |
| read_buffer_event.total_num_acl_data_packets().Write(host_max); |
| } |
| |
| l2cap_channel_manager_.ForceDrainChannelQueues(); |
| } |
| |
| template <class EventT> |
| void AclDataChannel::ProcessSpecificLEReadBufferSizeCommandCompleteEvent( |
| EventT read_buffer_event) { |
| { |
| std::lock_guard lock(credit_mutex_); |
| const uint16_t controller_max = |
| read_buffer_event.total_num_le_acl_data_packets().Read(); |
| // TODO: https://pwbug.dev/380316252 - Support shared buffers. |
| const uint16_t host_max = le_credits_.Reserve(controller_max); |
| read_buffer_event.total_num_le_acl_data_packets().Write(host_max); |
| } |
| |
| const uint16_t le_acl_data_packet_length = |
| read_buffer_event.le_acl_data_packet_length().Read(); |
| // TODO: https://pwbug.dev/380316252 - Support shared buffers. |
| if (le_acl_data_packet_length == 0) { |
| PW_LOG_ERROR( |
| "Controller shares data buffers between BR/EDR and LE transport, which " |
| "is not yet supported. So channels on LE transport will not be " |
| "functional."); |
| } |
| l2cap_channel_manager_.set_le_acl_data_packet_length( |
| le_acl_data_packet_length); |
| // Send packets that may have queued before we acquired any LE ACL credits. |
| l2cap_channel_manager_.ForceDrainChannelQueues(); |
| } |
| |
| template void |
| AclDataChannel::ProcessSpecificLEReadBufferSizeCommandCompleteEvent< |
| emboss::LEReadBufferSizeV1CommandCompleteEventWriter>( |
| emboss::LEReadBufferSizeV1CommandCompleteEventWriter read_buffer_event); |
| |
| template void |
| AclDataChannel::ProcessSpecificLEReadBufferSizeCommandCompleteEvent< |
| emboss::LEReadBufferSizeV2CommandCompleteEventWriter>( |
| emboss::LEReadBufferSizeV2CommandCompleteEventWriter read_buffer_event); |
| |
| void AclDataChannel::HandleNumberOfCompletedPacketsEvent( |
| H4PacketWithHci&& h4_packet) { |
| Result<emboss::NumberOfCompletedPacketsEventWriter> nocp_event = |
| MakeEmbossWriter<emboss::NumberOfCompletedPacketsEventWriter>( |
| h4_packet.GetHciSpan()); |
| if (!nocp_event.ok()) { |
| PW_LOG_ERROR( |
| "Buffer is too small for NUMBER_OF_COMPLETED_PACKETS event. So " |
| "will not process."); |
| hci_transport_.SendToHost(std::move(h4_packet)); |
| return; |
| } |
| |
| bool should_send_to_host = false; |
| bool did_reclaim_credits = false; |
| { |
| std::lock_guard lock(connection_mutex_); |
| for (uint8_t i = 0; i < nocp_event->num_handles().Read(); ++i) { |
| uint16_t handle = nocp_event->nocp_data()[i].connection_handle().Read(); |
| uint16_t num_completed_packets = |
| nocp_event->nocp_data()[i].num_completed_packets().Read(); |
| |
| if (num_completed_packets == 0) { |
| continue; |
| } |
| |
| AclConnection* connection_ptr = FindAclConnection(handle); |
| if (!connection_ptr) { |
| // Credits for connection we are not tracking or closed connection, so |
| // should pass event on to host. |
| should_send_to_host = true; |
| continue; |
| } |
| |
| // Reclaim proxy's credits before event is forwarded to host |
| uint16_t num_pending_packets = connection_ptr->num_pending_packets(); |
| uint16_t num_reclaimed = |
| std::min(num_completed_packets, num_pending_packets); |
| |
| if (num_reclaimed > 0) { |
| did_reclaim_credits = true; |
| } |
| |
| { |
| std::lock_guard credit_lock(credit_mutex_); |
| LookupCredits(connection_ptr->transport()).MarkCompleted(num_reclaimed); |
| } |
| |
| connection_ptr->set_num_pending_packets(num_pending_packets - |
| num_reclaimed); |
| |
| uint16_t credits_remaining = num_completed_packets - num_reclaimed; |
| nocp_event->nocp_data()[i].num_completed_packets().Write( |
| credits_remaining); |
| if (credits_remaining > 0) { |
| // Connection has credits remaining, so should past event on to host. |
| should_send_to_host = true; |
| } |
| } |
| } |
| |
| if (did_reclaim_credits) { |
| l2cap_channel_manager_.ForceDrainChannelQueues(); |
| } |
| if (should_send_to_host) { |
| hci_transport_.SendToHost(std::move(h4_packet)); |
| } |
| } |
| |
| void AclDataChannel::HandleConnectionCompleteEvent( |
| H4PacketWithHci&& h4_packet) { |
| pw::span<uint8_t> hci_buffer = h4_packet.GetHciSpan(); |
| Result<emboss::ConnectionCompleteEventView> connection_complete_event = |
| MakeEmbossView<emboss::ConnectionCompleteEventView>(hci_buffer); |
| if (!connection_complete_event.ok()) { |
| hci_transport_.SendToHost(std::move(h4_packet)); |
| return; |
| } |
| |
| if (connection_complete_event->status().Read() != |
| emboss::StatusCode::SUCCESS) { |
| hci_transport_.SendToHost(std::move(h4_packet)); |
| return; |
| } |
| |
| const uint16_t conn_handle = |
| connection_complete_event->connection_handle().Read(); |
| |
| if (CreateAclConnection(conn_handle, AclTransportType::kBrEdr) == |
| Status::ResourceExhausted()) { |
| PW_LOG_ERROR( |
| "Could not track connection like requested. Max connections " |
| "reached."); |
| } |
| |
| hci_transport_.SendToHost(std::move(h4_packet)); |
| } |
| |
| void AclDataChannel::HandleLeConnectionCompleteEvent( |
| uint16_t connection_handle, emboss::StatusCode status) { |
| if (status != emboss::StatusCode::SUCCESS) { |
| return; |
| } |
| |
| if (CreateAclConnection(connection_handle, AclTransportType::kLe) == |
| Status::ResourceExhausted()) { |
| PW_LOG_ERROR( |
| "Could not track connection like requested. Max connections " |
| "reached."); |
| } |
| } |
| |
| void AclDataChannel::HandleLeConnectionCompleteEvent( |
| H4PacketWithHci&& h4_packet) { |
| pw::span<uint8_t> hci_buffer = h4_packet.GetHciSpan(); |
| Result<emboss::LEConnectionCompleteSubeventView> event = |
| MakeEmbossView<emboss::LEConnectionCompleteSubeventView>(hci_buffer); |
| if (!event.ok()) { |
| hci_transport_.SendToHost(std::move(h4_packet)); |
| return; |
| } |
| |
| HandleLeConnectionCompleteEvent(event->connection_handle().Read(), |
| event->status().Read()); |
| |
| hci_transport_.SendToHost(std::move(h4_packet)); |
| } |
| |
| void AclDataChannel::HandleLeEnhancedConnectionCompleteV1Event( |
| H4PacketWithHci&& h4_packet) { |
| pw::span<uint8_t> hci_buffer = h4_packet.GetHciSpan(); |
| Result<emboss::LEEnhancedConnectionCompleteSubeventV1View> event = |
| MakeEmbossView<emboss::LEEnhancedConnectionCompleteSubeventV1View>( |
| hci_buffer); |
| if (!event.ok()) { |
| hci_transport_.SendToHost(std::move(h4_packet)); |
| return; |
| } |
| |
| HandleLeConnectionCompleteEvent(event->connection_handle().Read(), |
| event->status().Read()); |
| |
| hci_transport_.SendToHost(std::move(h4_packet)); |
| } |
| |
| void AclDataChannel::HandleLeEnhancedConnectionCompleteV2Event( |
| H4PacketWithHci&& h4_packet) { |
| pw::span<uint8_t> hci_buffer = h4_packet.GetHciSpan(); |
| Result<emboss::LEEnhancedConnectionCompleteSubeventV2View> event = |
| MakeEmbossView<emboss::LEEnhancedConnectionCompleteSubeventV2View>( |
| hci_buffer); |
| if (!event.ok()) { |
| hci_transport_.SendToHost(std::move(h4_packet)); |
| return; |
| } |
| |
| HandleLeConnectionCompleteEvent(event->connection_handle().Read(), |
| event->status().Read()); |
| |
| hci_transport_.SendToHost(std::move(h4_packet)); |
| } |
| |
| void AclDataChannel::ProcessDisconnectionCompleteEvent( |
| pw::span<uint8_t> hci_span) { |
| Result<emboss::DisconnectionCompleteEventView> dc_event = |
| MakeEmbossView<emboss::DisconnectionCompleteEventView>(hci_span); |
| if (!dc_event.ok()) { |
| PW_LOG_ERROR( |
| "Buffer is too small for DISCONNECTION_COMPLETE event. So will not " |
| "process."); |
| return; |
| } |
| |
| { |
| std::lock_guard lock(connection_mutex_); |
| uint16_t conn_handle = dc_event->connection_handle().Read(); |
| |
| AclConnection* connection_ptr = FindAclConnection(conn_handle); |
| |
| if (!connection_ptr) { |
| PW_LOG_WARN( |
| "btproxy: Viewed disconnect (reason: %#.2hhx) for connection %#x, " |
| "but was unable to find an existing open AclConnection.", |
| cpp23::to_underlying(dc_event->reason().Read()), |
| conn_handle); |
| return; |
| } |
| |
| emboss::StatusCode status = dc_event->status().Read(); |
| if (status == emboss::StatusCode::SUCCESS) { |
| PW_LOG_INFO( |
| "Proxy viewed disconnect (reason: %#.2hhx) for connection %#x.", |
| cpp23::to_underlying(dc_event->reason().Read()), |
| conn_handle); |
| if (connection_ptr->num_pending_packets() > 0) { |
| PW_LOG_WARN( |
| "Connection %#x is disconnecting with packets in flight. Releasing " |
| "associated credits.", |
| conn_handle); |
| std::lock_guard credit_lock(credit_mutex_); |
| LookupCredits(connection_ptr->transport()) |
| .MarkCompleted(connection_ptr->num_pending_packets()); |
| } |
| |
| l2cap_channel_manager_.HandleAclDisconnectionComplete(conn_handle); |
| acl_connections_.erase(connection_ptr); |
| } else { // Failed disconnect status |
| if (connection_ptr->num_pending_packets() > 0) { |
| PW_LOG_WARN( |
| "Proxy viewed failed disconnect (status: %#.2hhx) for connection " |
| "%#x with packets in flight. Not releasing associated credits.", |
| cpp23::to_underlying(status), |
| conn_handle); |
| } |
| } |
| } |
| } |
| |
| bool AclDataChannel::HasSendAclCapability(AclTransportType transport) const { |
| std::lock_guard lock(credit_mutex_); |
| return LookupCredits(transport).HasSendCapability(); |
| } |
| |
| uint16_t AclDataChannel::GetNumFreeAclPackets( |
| AclTransportType transport) const { |
| std::lock_guard lock(credit_mutex_); |
| return LookupCredits(transport).Remaining(); |
| } |
| |
| std::optional<AclDataChannel::SendCredit> AclDataChannel::ReserveSendCredit( |
| AclTransportType transport) { |
| std::lock_guard lock(credit_mutex_); |
| if (const auto status = LookupCredits(transport).MarkPending(1); |
| !status.ok()) { |
| return std::nullopt; |
| } |
| return SendCredit(transport, [this](AclTransportType t) { |
| std::lock_guard fn_lock(credit_mutex_); |
| LookupCredits(t).MarkCompleted(1); |
| }); |
| } |
| |
| pw::Status AclDataChannel::SendAcl(H4PacketWithH4&& h4_packet, |
| SendCredit&& credit) { |
| std::lock_guard lock(connection_mutex_); |
| Result<emboss::AclDataFrameHeaderView> acl_view = |
| MakeEmbossView<emboss::AclDataFrameHeaderView>(h4_packet.GetHciSpan()); |
| if (!acl_view.ok()) { |
| PW_LOG_ERROR("An invalid ACL packet was provided. So will not send."); |
| return pw::Status::InvalidArgument(); |
| } |
| uint16_t handle = acl_view->handle().Read(); |
| |
| AclConnection* connection_ptr = FindAclConnection(handle); |
| if (!connection_ptr) { |
| PW_LOG_ERROR("Tried to send ACL packet on unregistered connection."); |
| return pw::Status::NotFound(); |
| } |
| |
| if (connection_ptr->transport() != credit.transport_) { |
| PW_LOG_WARN("Provided credit for wrong transport. So will not send."); |
| return pw::Status::InvalidArgument(); |
| } |
| credit.MarkUsed(); |
| |
| connection_ptr->set_num_pending_packets( |
| connection_ptr->num_pending_packets() + 1); |
| |
| hci_transport_.SendToController(std::move(h4_packet)); |
| return pw::OkStatus(); |
| } |
| |
| Status AclDataChannel::CreateAclConnection(uint16_t connection_handle, |
| AclTransportType transport) { |
| std::lock_guard lock(connection_mutex_); |
| AclConnection* connection_it = FindAclConnection(connection_handle); |
| if (connection_it) { |
| return Status::AlreadyExists(); |
| } |
| if (acl_connections_.full()) { |
| PW_LOG_ERROR( |
| "btproxy: Attempt to create new AclConnection when acl_connections_ is" |
| "already full. connection_handle: %#x", |
| connection_handle); |
| return Status::ResourceExhausted(); |
| } |
| acl_connections_.emplace_back(transport, |
| /*connection_handle=*/connection_handle, |
| /*num_pending_packets=*/0, |
| l2cap_channel_manager_); |
| return OkStatus(); |
| } |
| |
| L2capSignalingChannel* AclDataChannel::FindSignalingChannel( |
| uint16_t connection_handle, uint16_t local_cid) { |
| std::lock_guard lock(connection_mutex_); |
| |
| AclConnection* connection_ptr = FindAclConnection(connection_handle); |
| if (!connection_ptr) { |
| return nullptr; |
| } |
| |
| if (local_cid == connection_ptr->signaling_channel()->local_cid()) { |
| return connection_ptr->signaling_channel(); |
| } |
| return nullptr; |
| } |
| |
| AclDataChannel::AclConnection* AclDataChannel::FindAclConnection( |
| uint16_t connection_handle) { |
| AclConnection* connection_it = containers::FindIf( |
| acl_connections_, [connection_handle](const AclConnection& connection) { |
| return connection.connection_handle() == connection_handle; |
| }); |
| return connection_it == acl_connections_.end() ? nullptr : connection_it; |
| } |
| |
| namespace { |
| |
| std::optional<LockedL2capChannel> GetLockedChannel( |
| Direction direction, |
| uint16_t handle, |
| uint16_t l2cap_channel_id, |
| L2capChannelManager& manager) { |
| std::optional<LockedL2capChannel> channel; |
| |
| switch (direction) { |
| case Direction::kFromController: |
| return manager.FindChannelByLocalCid(handle, l2cap_channel_id); |
| case Direction::kFromHost: |
| return manager.FindChannelByRemoteCid(handle, l2cap_channel_id); |
| default: |
| PW_LOG_ERROR("Unrecognized Direction enumerator value %d.", |
| cpp23::to_underlying(direction)); |
| return std::nullopt; |
| } |
| } |
| |
| } // namespace |
| |
| bool AclDataChannel::HandleAclData(Direction direction, |
| emboss::AclDataFrameWriter& acl) { |
| // This function returns whether or not the frame was handled here. |
| // * Return true if the frame was handled by the proxy and should _not_ be |
| // passed on to the other side (Host/Controller). |
| // * Return false if the frame was _not_ handled by the proxy and should be |
| // passed on to the other side (Host/Controller). |
| // |
| // Special care needs to be taken when handling fragments. We don't want the |
| // proxy to consume an initial fragment, and then decide to pass a subsequent |
| // fragment because we didn't like it. That would cause the receiver to see |
| // an unexpected CONTINUING_FRAGMENT. |
| // |
| // This ACL frame could contain |
| // * A complete L2CAP PDU... |
| // * for an unrecognized channel -> Pass |
| // * for a recognized channel -> Handle and Consume |
| // |
| // * An initial fragment (w/ complete L2CAP header)... |
| // * while already recombining -> Stop recombination and Pass(?) |
| // * for an unrecognized channel -> Pass |
| // * for a recognized channel -> Start recombination and Consume |
| // |
| // * A subsequent fragment (CONTINUING_FRAGMENT)... |
| // * while recombining -> Recombine fragment and Consume |
| // (we know this must be for an L2CAP channel we care about) |
| // * while not recombining -> Pass |
| // |
| // TODO: https://pwbug.dev/392666078 - Consider refactoring to look like |
| // L2capCoc::ProcessPduFromControllerMultibuf() if we are okay with |
| // allocating and copying for every PDU. |
| static constexpr bool kHandled = true; |
| static constexpr bool kUnhandled = false; |
| |
| const uint16_t handle = acl.header().handle().Read(); |
| |
| bool is_first = false; |
| bool is_fragment = false; |
| |
| // Set once we know CID from the first packet or from recombiner. |
| uint16_t local_cid; |
| |
| // TODO: https://pwbug.dev/392665312 - make this <const uint8_t> |
| const pw::span<uint8_t> acl_payload{ |
| acl.payload().BackingStorage().data(), |
| acl.payload().BackingStorage().SizeInBytes()}; |
| |
| { |
| // Connection_mutex_ is expected to be acquired before/outside |
| // channels_mutex_. |
| // TODO: https://pwbug.dev/404344433 - Look at how we can ensure this with |
| // thread safety analysis. |
| std::lock_guard lock(connection_mutex_); |
| AclConnection* connection = FindAclConnection(handle); |
| if (!connection) { |
| return kUnhandled; |
| } |
| Recombiner& recombiner = connection->GetRecombiner(direction); |
| |
| // Is this a fragment? |
| const emboss::AclDataPacketBoundaryFlag boundary_flag = |
| acl.header().packet_boundary_flag().Read(); |
| switch (boundary_flag) { |
| // A subsequent fragment of a fragmented PDU. |
| case emboss::AclDataPacketBoundaryFlag::CONTINUING_FRAGMENT: { |
| // If recombination is not active, these are probably fragments for a |
| // PDU that we previously chose not to recombine. Simply ignore them. |
| // |
| // TODO: https://pwbug.dev/393417198 - This could also be an erroneous |
| // continuation of an already-recombined PDU, which would be better to |
| // drop. |
| if (!recombiner.IsActive()) { |
| return kUnhandled; |
| } |
| |
| local_cid = recombiner.local_cid(); |
| |
| is_fragment = true; |
| break; |
| } |
| // Non-fragment or the first fragment of a fragmented PDU. |
| case emboss::AclDataPacketBoundaryFlag::FIRST_NON_FLUSHABLE: |
| case emboss::AclDataPacketBoundaryFlag::FIRST_FLUSHABLE: { |
| is_first = true; |
| |
| // Ensure recombination is not already in progress |
| if (recombiner.IsActive()) { |
| PW_LOG_WARN( |
| "Received non-continuation packet %s on connection %#x while " |
| "recombination is active! Dropping previous partially-recombined " |
| "PDU and handling this first packet normally.", |
| DirectionToString(direction), |
| handle); |
| |
| // Note this conditionally acquires channels_mutex_ which, if nested, |
| // is expected to be acquired after/inside connection_mutex_. |
| std::optional<LockedL2capChannel> channel = |
| GetLockedChannel(direction, |
| handle, |
| recombiner.local_cid(), |
| l2cap_channel_manager_); |
| recombiner.EndRecombination(channel); |
| } // recombiner.IsActive(). channel with channels_mutex_ released. |
| |
| // Currently, we require the full L2CAP header: We need the pdu_length |
| // field so we know how much data to recombine, and we need the |
| // channel_id field so we know whether or not this is a recognized L2CAP |
| // channel and therefore whether or not we should recombine it. |
| // TODO: https://pwbug.dev/392652874 - Handle fragmented L2CAP header. |
| emboss::BasicL2capHeaderView l2cap_header = |
| emboss::MakeBasicL2capHeaderView(acl_payload.data(), |
| acl_payload.size()); |
| if (!l2cap_header.Ok()) { |
| PW_LOG_ERROR( |
| "ACL packet %s on connection %#x does not include full L2CAP " |
| "header. Passing on.", |
| DirectionToString(direction), |
| handle); |
| return kUnhandled; |
| } |
| |
| local_cid = l2cap_header.channel_id().Read(); |
| |
| // Is this a channel we care about? |
| // Note this conditionally acquires channels_mutex_ which, if nested, |
| // is expected to be acquired after/inside connection_mutex_. |
| std::optional<LockedL2capChannel> channel = GetLockedChannel( |
| direction, handle, local_cid, l2cap_channel_manager_); |
| if (!channel.has_value()) { |
| return kUnhandled; |
| } |
| |
| const uint16_t acl_payload_size = acl.data_total_length().Read(); |
| |
| const uint16_t l2cap_frame_length = |
| emboss::BasicL2capHeader::IntrinsicSizeInBytes() + |
| l2cap_header.pdu_length().Read(); |
| |
| if (l2cap_frame_length < acl_payload_size) { |
| PW_LOG_ERROR( |
| "ACL packet %s for channel %#x on connection %#x has payload " |
| "(%u bytes) larger than specified L2CAP PDU size (%u bytes). " |
| "Dropping.", |
| DirectionToString(direction), |
| channel->channel().local_cid(), |
| handle, |
| acl_payload_size, |
| l2cap_frame_length); |
| return kHandled; |
| } |
| |
| // Is this the first fragment of a fragmented PDU? |
| // The first fragment is recognized when the L2CAP frame length exceeds |
| // the ACL frame data_total_length. |
| if (l2cap_frame_length > acl_payload_size) { |
| is_fragment = true; |
| |
| // Start recombination |
| pw::Status status = |
| recombiner.StartRecombination(*channel, l2cap_frame_length); |
| if (!status.ok()) { |
| // TODO: https://pwbug.dev/404275508 - This is an acquired channel, |
| // so need to do something different than just pass on to AP. |
| PW_LOG_ERROR( |
| "Cannot start recombination for L2capChannel %#x: " |
| "%s. Passing on.", |
| channel->channel().local_cid(), |
| status.str()); |
| return kUnhandled; |
| } |
| } |
| break; |
| } // FIRST_FLUSHABLE. channel with channels_mutex_ released. |
| |
| default: { |
| PW_LOG_ERROR( |
| "Packet %s on connection %#x: Unexpected ACL boundary flag: %u", |
| DirectionToString(direction), |
| handle, |
| cpp23::to_underlying(boundary_flag)); |
| return kUnhandled; |
| } |
| } |
| |
| if (is_fragment) { |
| // Recombine this fragment |
| |
| // If value, includes channels_mutex_ unique_lock. |
| std::optional<LockedL2capChannel> channel = GetLockedChannel( |
| direction, handle, local_cid, l2cap_channel_manager_); |
| |
| pw::Status recomb_status = |
| recombiner.RecombineFragment(channel, acl_payload); |
| if (!recomb_status.ok()) { |
| // Given that RecombinationActive is checked above, the only way this |
| // should fail is if the fragment is larger than expected, which can |
| // only happen on a continuing fragment, because the first fragment |
| // starts recombination above. |
| PW_DCHECK(!is_first); |
| |
| PW_LOG_ERROR( |
| "Received continuation packet %s for channel %#x on connection " |
| "%#x over specified PDU length. Dropping entire PDU.", |
| DirectionToString(direction), |
| local_cid, |
| handle); |
| recombiner.EndRecombination(channel); |
| return kHandled; // We own the channel; drop. |
| } |
| |
| if (!recombiner.IsComplete()) { |
| // We are done with this packet and awaiting the remaining fragments. |
| return kHandled; |
| } |
| |
| // Recombination complete! |
| // We will collect the recombination buffer from the channel below |
| // (outside the connection mutex). |
| |
| } // is_fragment. channel with channels_mutex_ released. |
| } // std::lock_guard lock(connection_mutex_) |
| |
| // At this point we have recombined a valid L2CAP frame. It may be |
| // from a single first ACL packet or a series of recombined ones (in which |
| // case we should be handling the last continuing packet). |
| PW_CHECK((is_first && !is_fragment) || (!is_first && is_fragment)); |
| |
| // But note, our return value only controls the disposition of the current ACL |
| // packet. |
| |
| // If value, includes channels_mutex_ unique_lock. |
| // We need channel for handling PDU and for recombine buffers. |
| // channels_mutex_ must be held as long as `recombined_mbuf` and |
| // `send_l2cap_pdu` are accessed to ensure channel is not destroyed. |
| std::optional<LockedL2capChannel> channel = |
| GetLockedChannel(direction, handle, local_cid, l2cap_channel_manager_); |
| |
| // If recombining, will be set with the recombined PDU. And must be held |
| // as long as `send_l2cap_pdu` is accessed. |
| std::optional<multibuf::MultiBuf> recombined_mbuf; |
| |
| // PDU we will actually send (will be set from first packet or from |
| // recombination). |
| pw::span<uint8_t> send_l2cap_pdu; |
| |
| if (!channel.has_value()) { |
| // We don't have the channel anymore. This indicates that the |
| // channel instance that recombination was started with has since been |
| // destroyed. So "drop" the PDU and handle the packet. |
| PW_LOG_INFO( |
| "Dropping first PDU %s original intended for channel %#x on connection " |
| "%#x since channel instance was destroyed by client since first packet " |
| "was received.", |
| DirectionToString(direction), |
| local_cid, |
| handle); |
| // TODO: https://pwbug.dev/402454277 - We might want to consider passing |
| // kUnhandled for "signaling" channels, but since we don't have the channel |
| // here we have no way to determine the channel type. Once we have shared |
| // channel refs we should revisit. |
| return kHandled; |
| } |
| |
| if (is_first) { |
| // We have whole PDU in first packet. |
| send_l2cap_pdu = acl_payload; |
| } else { |
| // We are a fragment, so we need to collect the recombined PDU from the |
| // channel. |
| |
| if (!Recombiner::HasBuf(channel, direction)) { |
| // To get here we must have a `channel`, but now we have found `channel |
| // doesn't have a recombination buf. This indicates `channel` is instance |
| // other than the one we started recombination with. So "drop" the PDU and |
| // handle the packet. |
| PW_LOG_INFO( |
| "Dropping recombined PDU %s original intended for channel %#x on " |
| "connection %#x since channel instance was destroyed by client since " |
| "first packet was received.", |
| DirectionToString(direction), |
| local_cid, |
| handle); |
| // TODO: https://pwbug.dev/392663102 - Revisit what best behavior is here |
| // when we work on support for rejecting a recombined L2CAP PDU. |
| return kHandled; |
| } |
| |
| // Store the recombined multibuf. |
| recombined_mbuf = Recombiner::TakeBuf(channel, direction); |
| // We must have had IsComplete above to get here, so buf should always have |
| // a value. |
| PW_CHECK(recombined_mbuf.has_value()); |
| // Confirm the MultiBuf is contiguous as expected. |
| PW_CHECK(recombined_mbuf->IsContiguous()); |
| |
| send_l2cap_pdu = |
| pw::span_cast<uint8_t>(recombined_mbuf->ContiguousSpan().value()); |
| |
| } // is_first else |
| |
| // Pass the L2CAP PDU on to the L2capChannel |
| // TODO: https://pwbug.dev/403567488 - Look at sending MultiBuf here rather |
| // than span. Channels at next level will create MultiBuf to pass on their |
| // payload any. |
| const bool result = |
| (direction == Direction::kFromController) |
| ? channel->channel().HandlePduFromController(send_l2cap_pdu) |
| : channel->channel().HandlePduFromHost(send_l2cap_pdu); |
| |
| if ((result == kUnhandled) && is_fragment) { |
| // Client rejected the entire PDU, but we just have the continuing packet |
| // with the last fragment. So we can't just return kUnhandled that would |
| // pass only this final fragment to the other side, and all preceding |
| // fragments would be missing. |
| // TODO: https://pwbug.dev/392663102 - Handle rejecting a recombined |
| // L2CAP PDU. |
| PW_LOG_ERROR( |
| "L2capChannel %#x indicates recombined PDU %s is unhandled, which is " |
| "unsupported. Dropping entire recombined PDU!", |
| local_cid, |
| DirectionToString(direction)); |
| return kHandled; |
| } |
| |
| // If value, releases unique_lock of channels_mutex_. |
| channel.reset(); |
| |
| // It's possible for a channel handling rx traffic to have queued tx traffic |
| // or events. So call `DrainChannelQueuesIfNewTx` and `DeliverPendingEvents` |
| // (outside of channels_mutex_ lock). |
| l2cap_channel_manager_.DrainChannelQueuesIfNewTx(); |
| l2cap_channel_manager_.DeliverPendingEvents(); |
| |
| return result; |
| } |
| |
| } // namespace pw::bluetooth::proxy |