1 // Copyright 2024 The Pigweed Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License"); you may not
4 // use this file except in compliance with the License. You may obtain a copy of
5 // the License at
6 //
7 // https://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 // License for the specific language governing permissions and limitations under
13 // the License.
14
15 #include "pw_bluetooth_proxy/l2cap_coc.h"
16
17 #include <cmath>
18 #include <cstdint>
19 #include <mutex>
20
21 #include "pw_assert/check.h"
22 #include "pw_bluetooth/emboss_util.h"
23 #include "pw_bluetooth/hci_data.emb.h"
24 #include "pw_bluetooth/l2cap_frames.emb.h"
25 #include "pw_bluetooth_proxy/h4_packet.h"
26 #include "pw_bluetooth_proxy/internal/l2cap_channel.h"
27 #include "pw_bluetooth_proxy/internal/l2cap_signaling_channel.h"
28 #include "pw_bluetooth_proxy/l2cap_channel_common.h"
29 #include "pw_log/log.h"
30 #include "pw_multibuf/multibuf.h"
31 #include "pw_status/status.h"
32
33 namespace pw::bluetooth::proxy {
34
35 namespace {
36
37 // TODO: b/353734827 - Allow client to determine this constant.
38 const float kRxCreditReplenishThreshold = 0.30;
39
40 } // namespace
41
L2capCoc(L2capCoc && other)42 L2capCoc::L2capCoc(L2capCoc&& other)
43 : L2capChannel(static_cast<L2capCoc&&>(other)),
44 signaling_channel_(other.signaling_channel_),
45 rx_mtu_(other.rx_mtu_),
46 rx_mps_(other.rx_mps_),
47 tx_mtu_(other.tx_mtu_),
48 tx_mps_(other.tx_mps_),
49 receive_fn_(std::move(other.receive_fn_)) {
50 {
51 std::lock_guard lock(tx_mutex_);
52 std::lock_guard other_lock(other.tx_mutex_);
53 tx_credits_ = other.tx_credits_;
54 }
55 {
56 std::lock_guard lock(rx_mutex_);
57 std::lock_guard other_lock(other.rx_mutex_);
58 remaining_sdu_bytes_to_ignore_ = other.remaining_sdu_bytes_to_ignore_;
59 rx_sdu_ = std::move(other.rx_sdu_);
60 rx_sdu_offset_ = other.rx_sdu_offset_;
61 rx_sdu_bytes_remaining_ = other.rx_sdu_bytes_remaining_;
62 rx_remaining_credits_ = other.rx_remaining_credits_;
63 rx_total_credits_ = other.rx_total_credits_;
64 }
65 }
66
Write(multibuf::MultiBuf && payload)67 StatusWithMultiBuf L2capCoc::Write(multibuf::MultiBuf&& payload) {
68 if (payload.size() > tx_mtu_) {
69 PW_LOG_ERROR(
70 "Payload (%zu bytes) exceeds MTU (%d bytes). So will not process.",
71 payload.size(),
72 tx_mtu_);
73 return {Status::InvalidArgument(), std::move(payload)};
74 }
75
76 return L2capChannel::Write(std::move(payload));
77 }
78
Create(pw::multibuf::MultiBufAllocator & rx_multibuf_allocator,L2capChannelManager & l2cap_channel_manager,L2capSignalingChannel * signaling_channel,uint16_t connection_handle,CocConfig rx_config,CocConfig tx_config,ChannelEventCallback && event_fn,Function<void (multibuf::MultiBuf && payload)> && receive_fn)79 pw::Result<L2capCoc> L2capCoc::Create(
80 pw::multibuf::MultiBufAllocator& rx_multibuf_allocator,
81 L2capChannelManager& l2cap_channel_manager,
82 L2capSignalingChannel* signaling_channel,
83 uint16_t connection_handle,
84 CocConfig rx_config,
85 CocConfig tx_config,
86 ChannelEventCallback&& event_fn,
87 Function<void(multibuf::MultiBuf&& payload)>&& receive_fn) {
88 if (!AreValidParameters(/*connection_handle=*/connection_handle,
89 /*local_cid=*/rx_config.cid,
90 /*remote_cid=*/tx_config.cid)) {
91 return pw::Status::InvalidArgument();
92 }
93
94 if (tx_config.mps < emboss::L2capLeCreditBasedConnectionReq::min_mps() ||
95 tx_config.mps > emboss::L2capLeCreditBasedConnectionReq::max_mps()) {
96 PW_LOG_ERROR(
97 "Tx MPS (%d octets) invalid. L2CAP implementations shall support a "
98 "minimum MPS of 23 octets and may support an MPS up to 65533 octets.",
99 tx_config.mps);
100 return pw::Status::InvalidArgument();
101 }
102
103 return L2capCoc(
104 /*rx_multibuf_allocator=*/rx_multibuf_allocator,
105 /*l2cap_channel_manager=*/l2cap_channel_manager,
106 /*signaling_channel=*/signaling_channel,
107 /*connection_handle=*/connection_handle,
108 /*rx_config=*/rx_config,
109 /*tx_config=*/tx_config,
110 /*event_fn=*/std::move(event_fn),
111 /*receive_fn=*/std::move(receive_fn));
112 }
113
ReplenishRxCredits(uint16_t additional_rx_credits)114 pw::Status L2capCoc::ReplenishRxCredits(uint16_t additional_rx_credits) {
115 if (!signaling_channel_) {
116 return Status::FailedPrecondition();
117 }
118 PW_CHECK(rx_multibuf_allocator());
119 // SendFlowControlCreditInd logs if status is not ok, so no need to log here.
120 return signaling_channel_->SendFlowControlCreditInd(
121 local_cid(), additional_rx_credits, *rx_multibuf_allocator());
122 }
123
SendAdditionalRxCredits(uint16_t additional_rx_credits)124 pw::Status L2capCoc::SendAdditionalRxCredits(uint16_t additional_rx_credits) {
125 if (state() != State::kRunning) {
126 return Status::FailedPrecondition();
127 }
128 std::lock_guard lock(rx_mutex_);
129 Status status = ReplenishRxCredits(additional_rx_credits);
130
131 if (status.ok()) {
132 // We treat additional bumps from the client as bumping the total allowed
133 // credits.
134 rx_total_credits_ += additional_rx_credits;
135 rx_remaining_credits_ += additional_rx_credits;
136 PW_LOG_INFO(
137 "btproxy: L2capCoc::SendAdditionalRxCredits - status: %s, "
138 "additional_rx_credits: %u, rx_total_credits_: %u, "
139 "rx_remaining_credits_: %u",
140 status.str(),
141 additional_rx_credits,
142 rx_total_credits_,
143 rx_remaining_credits_);
144 }
145 DrainChannelQueuesIfNewTx();
146 return status;
147 }
148
DoHandlePduFromController(pw::span<uint8_t> kframe)149 bool L2capCoc::DoHandlePduFromController(pw::span<uint8_t> kframe) {
150 if (state() != State::kRunning) {
151 PW_LOG_ERROR(
152 "btproxy: L2capCoc::HandlePduFromController on non-running "
153 "channel. local_cid: %u, remote_cid: %u, state: %u",
154 local_cid(),
155 remote_cid(),
156 cpp23::to_underlying(state()));
157 StopAndSendEvent(L2capChannelEvent::kRxWhileStopped);
158 return true;
159 }
160
161 std::lock_guard lock(rx_mutex_);
162 rx_remaining_credits_--;
163
164 uint16_t rx_credits_used = rx_total_credits_ - rx_remaining_credits_;
165 if (rx_credits_used >=
166 std::ceil(rx_total_credits_ * kRxCreditReplenishThreshold)) {
167 Status status = ReplenishRxCredits(rx_credits_used);
168 if (status.IsUnavailable()) {
169 PW_LOG_INFO(
170 "Unable to send %hu rx credits to remote (it has %hu credits "
171 "remaining). Will try on next PDU receive.",
172 rx_credits_used,
173 rx_total_credits_);
174 } else if (status.IsFailedPrecondition()) {
175 PW_LOG_WARN(
176 "Unable to send rx credits to remote, perhaps the connection has "
177 "been closed?");
178 } else {
179 PW_CHECK(status.ok());
180 rx_remaining_credits_ += rx_credits_used;
181 }
182 }
183
184 ConstByteSpan kframe_payload;
185 if (rx_sdu_bytes_remaining_ > 0) {
186 // Received PDU that is part of current SDU being assembled.
187 Result<emboss::SubsequentKFrameView> subsequent_kframe_view =
188 MakeEmbossView<emboss::SubsequentKFrameView>(kframe);
189 // Lower layers should not (and cannot) invoke this callback on a packet
190 // with an incomplete basic L2CAP header.
191 PW_CHECK_OK(subsequent_kframe_view);
192
193 // Core Spec v6.0 Vol 3, Part A, 3.4.3: "If the payload size of any K-frame
194 // exceeds the receiver's MPS, the receiver shall disconnect the channel."
195 uint16_t payload_size = subsequent_kframe_view->payload_size().Read();
196 if (payload_size > rx_mps_) {
197 PW_LOG_ERROR(
198 "(CID %#x) Rx K-frame payload exceeds MPU. So stopping channel & "
199 "reporting it needs to be closed.",
200 local_cid());
201 StopAndSendEvent(L2capChannelEvent::kRxInvalid);
202 return true;
203 }
204
205 kframe_payload =
206 as_bytes(span(subsequent_kframe_view->payload().BackingStorage().data(),
207 subsequent_kframe_view->payload_size().Read()));
208 } else {
209 // Received first (or only) PDU of SDU.
210 Result<emboss::FirstKFrameView> first_kframe_view =
211 MakeEmbossView<emboss::FirstKFrameView>(kframe);
212 if (!first_kframe_view.ok()) {
213 PW_LOG_ERROR(
214 "(CID %#x) Buffer is too small for first K-frame. So stopping "
215 "channel and reporting it needs to be closed.",
216 local_cid());
217 StopAndSendEvent(L2capChannelEvent::kRxInvalid);
218 return true;
219 }
220
221 rx_sdu_bytes_remaining_ = first_kframe_view->sdu_length().Read();
222
223 // Core Spec v6.0 Vol 3, Part A, 3.4.3: "If the SDU length field value
224 // exceeds the receiver's MTU, the receiver shall disconnect the channel."
225 if (rx_sdu_bytes_remaining_ > rx_mtu_) {
226 PW_LOG_ERROR(
227 "(CID %#x) Rx K-frame SDU exceeds MTU. So stopping channel & "
228 "reporting it needs to be closed.",
229 local_cid());
230 StopAndSendEvent(L2capChannelEvent::kRxInvalid);
231 return true;
232 }
233
234 // Core Spec v6.0 Vol 3, Part A, 3.4.3: "If the payload size of any K-frame
235 // exceeds the receiver's MPS, the receiver shall disconnect the channel."
236 uint16_t payload_size = first_kframe_view->payload_size().Read();
237 if (payload_size > rx_mps_) {
238 PW_LOG_ERROR(
239 "(CID %#x) Rx K-frame payload exceeds MPU. So stopping channel & "
240 "reporting it needs to be closed.",
241 local_cid());
242 StopAndSendEvent(L2capChannelEvent::kRxInvalid);
243 return true;
244 }
245
246 rx_sdu_ =
247 rx_multibuf_allocator()->AllocateContiguous(rx_sdu_bytes_remaining_);
248 if (!rx_sdu_) {
249 PW_LOG_ERROR(
250 "(CID %#x) Rx MultiBuf allocator out of memory. So stopping channel "
251 "and reporting it needs to be closed.",
252 local_cid());
253 StopAndSendEvent(L2capChannelEvent::kRxOutOfMemory);
254 return true;
255 }
256
257 kframe_payload =
258 as_bytes(span(first_kframe_view->payload().BackingStorage().data(),
259 first_kframe_view->payload_size().Read()));
260 }
261
262 // Copy segment into rx_sdu_.
263 StatusWithSize status = rx_sdu_->CopyFrom(/*source=*/kframe_payload,
264 /*position=*/rx_sdu_offset_);
265 if (status.IsResourceExhausted()) {
266 // Core Spec v6.0 Vol 3, Part A, 3.4.3: "If the sum of the payload sizes
267 // for the K-frames exceeds the specified SDU length, the receiver shall
268 // disconnect the channel."
269 PW_LOG_ERROR(
270 "(CID %#x) Sum of K-frame payload sizes exceeds the specified SDU "
271 "length. So stopping channel and reporting it needs to be closed.",
272 local_cid());
273 StopAndSendEvent(L2capChannelEvent::kRxInvalid);
274 return true;
275 }
276 PW_CHECK_OK(status);
277
278 rx_sdu_bytes_remaining_ -= kframe_payload.size();
279 rx_sdu_offset_ += kframe_payload.size();
280
281 if (rx_sdu_bytes_remaining_ == 0) {
282 // We have a full SDU, so invoke client callback.
283 if (receive_fn_) {
284 receive_fn_(std::move(*rx_sdu_));
285 }
286 rx_sdu_ = std::nullopt;
287 rx_sdu_offset_ = 0;
288 }
289
290 return true;
291 }
292
HandlePduFromHost(pw::span<uint8_t>)293 bool L2capCoc::HandlePduFromHost(pw::span<uint8_t>) {
294 // Always forward data from host to controller
295 return false;
296 }
297
DoClose()298 void L2capCoc::DoClose() {
299 std::lock_guard lock(rx_mutex_);
300 signaling_channel_ = nullptr;
301 }
302
L2capCoc(pw::multibuf::MultiBufAllocator & rx_multibuf_allocator,L2capChannelManager & l2cap_channel_manager,L2capSignalingChannel * signaling_channel,uint16_t connection_handle,CocConfig rx_config,CocConfig tx_config,ChannelEventCallback && event_fn,Function<void (multibuf::MultiBuf && payload)> && receive_fn)303 L2capCoc::L2capCoc(pw::multibuf::MultiBufAllocator& rx_multibuf_allocator,
304 L2capChannelManager& l2cap_channel_manager,
305 L2capSignalingChannel* signaling_channel,
306 uint16_t connection_handle,
307 CocConfig rx_config,
308 CocConfig tx_config,
309 ChannelEventCallback&& event_fn,
310 Function<void(multibuf::MultiBuf&& payload)>&& receive_fn)
311 : L2capChannel(l2cap_channel_manager,
312 &rx_multibuf_allocator,
313 /*connection_handle=*/connection_handle,
314 /*transport=*/AclTransportType::kLe,
315 /*local_cid=*/rx_config.cid,
316 /*remote_cid=*/tx_config.cid,
317 /*payload_from_controller_fn=*/nullptr,
318 /*payload_from_host_fn=*/nullptr,
319 /*event_fn=*/std::move(event_fn)),
320
321 signaling_channel_(signaling_channel),
322 rx_mtu_(rx_config.mtu),
323 rx_mps_(rx_config.mps),
324 tx_mtu_(tx_config.mtu),
325 tx_mps_(tx_config.mps),
326 receive_fn_(std::move(receive_fn)),
327 rx_remaining_credits_(rx_config.credits),
328 rx_total_credits_(rx_config.credits),
329 tx_credits_(tx_config.credits) {
330 PW_LOG_INFO(
331 "btproxy: L2capCoc ctor - rx_remaining_credits_: %u, "
332 "rx_total_credits_: %u, tx_credits_: %u",
333 rx_remaining_credits_,
334 rx_total_credits_,
335 tx_credits_);
336 }
337
~L2capCoc()338 L2capCoc::~L2capCoc() {
339 // Don't log dtor of moved-from channels.
340 if (state() != State::kUndefined) {
341 PW_LOG_INFO("btproxy: L2capCoc dtor");
342 }
343 }
344
MaxL2capPayloadSize() const345 std::optional<uint16_t> L2capCoc::MaxL2capPayloadSize() const {
346 std::optional<uint16_t> max_l2cap_payload_size =
347 L2capChannel::MaxL2capPayloadSize();
348 if (!max_l2cap_payload_size) {
349 return std::nullopt;
350 }
351 return std::min(*max_l2cap_payload_size, tx_mps_);
352 }
353
GenerateNextTxPacket()354 std::optional<H4PacketWithH4> L2capCoc::GenerateNextTxPacket() {
355 std::lock_guard lock(tx_mutex_);
356 constexpr uint8_t kSduLengthFieldSize = 2;
357 std::optional<uint16_t> max_l2cap_payload_size = MaxL2capPayloadSize();
358 if (state() != State::kRunning || PayloadQueueEmpty() || tx_credits_ == 0 ||
359 !max_l2cap_payload_size ||
360 *max_l2cap_payload_size <= kSduLengthFieldSize) {
361 return std::nullopt;
362 }
363
364 ConstByteSpan sdu_span = GetFrontPayloadSpan();
365 // Number of client SDU bytes to be encoded in this segment.
366 uint16_t sdu_bytes_in_segment;
367 // Size of PDU payload for this L2CAP frame.
368 uint16_t pdu_data_size;
369 if (!is_continuing_segment_) {
370 // Generating the first (or only) PDU of an SDU.
371 size_t sdu_bytes_max_allowable =
372 *max_l2cap_payload_size - kSduLengthFieldSize;
373 sdu_bytes_in_segment = std::min(sdu_span.size(), sdu_bytes_max_allowable);
374 pdu_data_size = sdu_bytes_in_segment + kSduLengthFieldSize;
375 } else {
376 // Generating a continuing PDU in an SDU.
377 size_t sdu_bytes_max_allowable = *max_l2cap_payload_size;
378 sdu_bytes_in_segment =
379 std::min(sdu_span.size() - tx_sdu_offset_, sdu_bytes_max_allowable);
380 pdu_data_size = sdu_bytes_in_segment;
381 }
382
383 pw::Result<H4PacketWithH4> h4_result = PopulateTxL2capPacket(pdu_data_size);
384 if (!h4_result.ok()) {
385 // This can fail if all H4 buffers are occupied.
386 return std::nullopt;
387 }
388 H4PacketWithH4 h4_packet = std::move(*h4_result);
389
390 Result<emboss::AclDataFrameWriter> acl =
391 MakeEmbossWriter<emboss::AclDataFrameWriter>(h4_packet.GetHciSpan());
392 PW_CHECK(acl.ok());
393
394 if (!is_continuing_segment_) {
395 Result<emboss::FirstKFrameWriter> first_kframe_writer =
396 MakeEmbossWriter<emboss::FirstKFrameWriter>(
397 acl->payload().BackingStorage().data(),
398 acl->payload().SizeInBytes());
399 PW_CHECK(first_kframe_writer.ok());
400 first_kframe_writer->sdu_length().Write(sdu_span.size());
401 PW_CHECK(first_kframe_writer->Ok());
402 PW_CHECK(TryToCopyToEmbossStruct(
403 /*emboss_dest=*/first_kframe_writer->payload(),
404 /*src=*/sdu_span.subspan(tx_sdu_offset_, sdu_bytes_in_segment)));
405 } else {
406 Result<emboss::SubsequentKFrameWriter> subsequent_kframe_writer =
407 MakeEmbossWriter<emboss::SubsequentKFrameWriter>(
408 acl->payload().BackingStorage().data(),
409 acl->payload().SizeInBytes());
410 PW_CHECK(subsequent_kframe_writer.ok());
411 PW_CHECK(TryToCopyToEmbossStruct(
412 /*emboss_dest=*/subsequent_kframe_writer->payload(),
413 /*src=*/sdu_span.subspan(tx_sdu_offset_, sdu_bytes_in_segment)));
414 }
415
416 tx_sdu_offset_ += sdu_bytes_in_segment;
417
418 if (tx_sdu_offset_ == sdu_span.size()) {
419 // This segment was the final (or only) PDU of the SDU.
420 PopFrontPayload();
421 tx_sdu_offset_ = 0;
422 is_continuing_segment_ = false;
423 } else {
424 is_continuing_segment_ = true;
425 }
426
427 --tx_credits_;
428 return h4_packet;
429 }
430
AddTxCredits(uint16_t credits)431 void L2capCoc::AddTxCredits(uint16_t credits) {
432 if (state() != State::kRunning) {
433 PW_LOG_ERROR(
434 "(CID %#x) Received credits on stopped CoC. So will ignore signal.",
435 local_cid());
436 return;
437 }
438
439 bool credits_previously_zero;
440 {
441 std::lock_guard lock(tx_mutex_);
442
443 // Core Spec v6.0 Vol 3, Part A, 10.1: "The device receiving the credit
444 // packet shall disconnect the L2CAP channel if the credit count exceeds
445 // 65535."
446 if (credits > emboss::L2capLeCreditBasedConnectionReq::max_credit_value() -
447 tx_credits_) {
448 PW_LOG_ERROR(
449 "btproxy: Received additional tx credits %u which put tx_credits_ %u "
450 "beyond max credit value of %ld. So stopping channel and reporting "
451 "it needs to be closed. local_cid: %u, remote_cid: %u",
452 credits,
453 tx_credits_,
454 long{emboss::L2capLeCreditBasedConnectionReq::max_credit_value()},
455 local_cid(),
456 remote_cid());
457 StopAndSendEvent(L2capChannelEvent::kRxInvalid);
458 return;
459 }
460
461 credits_previously_zero = tx_credits_ == 0;
462 tx_credits_ += credits;
463 }
464 if (credits_previously_zero) {
465 ReportNewTxPacketsOrCredits();
466 }
467 }
468
469 } // namespace pw::bluetooth::proxy
470