| // Copyright 2018 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 "session.h" |
| |
| #include <lib/async/default.h> |
| #include <lib/fit/defer.h> |
| |
| #include "garnet/drivers/bluetooth/lib/common/log.h" |
| #include "garnet/drivers/bluetooth/lib/common/slab_allocator.h" |
| #include "garnet/drivers/bluetooth/lib/rfcomm/rfcomm.h" |
| #include "garnet/drivers/bluetooth/lib/rfcomm/session.h" |
| |
| namespace btlib { |
| namespace rfcomm { |
| |
| namespace { |
| |
| // When the remote credit count drops below the low water mark, we will |
| // replenish. |
| constexpr Credits kLowWaterMark = 10; |
| |
| // This is the maximum amount of credits we will allow the remote Session to |
| // have. |
| constexpr Credits kHighWaterMark = 100; |
| |
| // Timeout system parameters (see RFCOMM 5.3, table 5.1) |
| // |
| // T1: timeout for (most) command frames; in RFCOMM, this only applies to SABM |
| // and DISC frames. 10-60 seconds, recommended value 20 seconds. |
| constexpr zx::duration kAcknowledgementTimer = zx::sec(20); |
| |
| // T1': timeout for SABM frames used to start DLCs with DLCI > 0. See RFCOMM |
| // 5.3. 60-300 seconds. |
| constexpr zx::duration kAcknowledgementTimerUserDLCs = zx::sec(300); |
| |
| // T2: timeout for multiplexer commands. 10-60 seconds, recommended value 20 |
| // seconds. |
| constexpr zx::duration kMuxResponseTimer = zx::sec(20); |
| |
| // The amount of time the multiplexer will wait when a startup conflict is |
| // detected. A conflict occurs when the local and remote multiplexers attempt to |
| // start at the same time. After delaying by the below amount, the local |
| // multiplexer will attempt to start up the multiplexer again. see RFCOMM 5.2.1. |
| constexpr zx::duration kMuxStartupConflictDelay = zx::msec(20); |
| |
| // Used to indicate that an outstanding multiplexer command does not pertain to |
| // any specific DLCI. This is applicable for multiplexer commands such as the |
| // Test command. |
| static constexpr DLCI kNoDLCI = 1; // 1 is an invalid DLCI. |
| |
| // Given a multiplexer command, find the DLCI which this command pertains to. |
| // Commands such as DLC Parameter Negotiation (PN) pertain to specific DLCs, |
| // whereas commands like the Test command or FCon/FCoff commands do not. |
| // Returns kNoDLCI for those commands which do not pertain to a specific DLCI. |
| DLCI GetDLCIFromMuxCommand(MuxCommand* mux_command) { |
| MuxCommandType type = mux_command->command_type(); |
| switch (type) { |
| case MuxCommandType::kDLCParameterNegotiation: |
| return static_cast<DLCParameterNegotiationCommand*>(mux_command) |
| ->params() |
| .dlci; |
| case MuxCommandType::kModemStatusCommand: |
| return static_cast<ModemStatusCommand*>(mux_command)->dlci(); |
| case MuxCommandType::kRemoteLineStatusCommand: |
| return static_cast<RemoteLineStatusCommand*>(mux_command)->dlci(); |
| case MuxCommandType::kRemotePortNegotiationCommand: |
| return static_cast<RemotePortNegotiationCommand*>(mux_command)->dlci(); |
| case MuxCommandType::kFlowControlOffCommand: |
| case MuxCommandType::kFlowControlOnCommand: |
| case MuxCommandType::kTestCommand: |
| case MuxCommandType::kNonSupportedCommandResponse: |
| return kNoDLCI; |
| default: |
| bt_log(ERROR, "rfcomm", "unexpected multiplexer command type"); |
| return kNoDLCI; |
| } |
| } |
| |
| // Returns true if this user DLCI "belongs to" the side of the session with the |
| // given |role|. See RFCOMM 5.2: "...this partitions the DLCI value space such |
| // that server applications on the non-initiating device are reachable on DLCIs |
| // 2,4,6,...,60, and server applications on the initiating device are reachable |
| // on 3,5,7,...,61." |
| constexpr bool IsValidLocalChannel(Role role, DLCI dlci) { |
| ZX_DEBUG_ASSERT(IsMultiplexerStarted(role)); |
| ZX_DEBUG_ASSERT(IsUserDLCI(dlci)); |
| return (role == Role::kInitiator ? 1 : 0) == dlci % 2; |
| } |
| |
| // Returns true if a frame should have credit-based flow control applied to it. |
| // Credit-based flow applies only to UIH frames containing a nonzero amount |
| // of user data. It does not apply to multiplexer commands. See RFCOMM 6.5. |
| bool CreditsApply(const Frame& frame) { |
| return (static_cast<FrameType>(frame.control()) == |
| FrameType::kUnnumberedInfoHeaderCheck) && |
| IsUserDLCI(frame.dlci()) && (frame.length() > 0); |
| } |
| |
| // Returns true if a frame can carry credits to the remote peer. |
| // Only UIH frames may contain a credit field. |
| // It only makes sense to carry credits on user DLCIs, the multiplexer DLCI is |
| // not credit-based flow controlled. |
| bool CreditsCandidate(Frame* frame) { |
| return IsUserDLCI(frame->dlci()) && |
| (static_cast<FrameType>(frame->control()) == |
| FrameType::kUnnumberedInfoHeaderCheck); |
| } |
| |
| } // namespace |
| |
| void Session::SendUserData(DLCI dlci, common::ByteBufferPtr data) { |
| ZX_DEBUG_ASSERT(!data || data->size() <= GetMaximumUserDataLength()); |
| bool sent = SendFrame(std::make_unique<UserDataFrame>( |
| role_, credit_based_flow_, dlci, std::move(data))); |
| ZX_DEBUG_ASSERT(sent); |
| } |
| |
| size_t Session::GetMaximumUserDataLength() const { |
| ZX_DEBUG_ASSERT(initial_param_negotiation_state_ == |
| ParameterNegotiationState::kNegotiated); |
| |
| // If credit-based flow is on, we always reserve a single octet for credits. |
| return credit_based_flow_ ? (maximum_frame_size_ - 1) : maximum_frame_size_; |
| } |
| |
| std::unique_ptr<Session> Session::Create( |
| fbl::RefPtr<l2cap::Channel> l2cap_channel, |
| ChannelOpenedCallback channel_opened_cb) { |
| auto session = |
| std::unique_ptr<Session>(new Session(std::move(channel_opened_cb))); |
| if (!session->SetL2CAPChannel(l2cap_channel)) |
| return nullptr; |
| return session; |
| } |
| |
| Session::Session(ChannelOpenedCallback channel_opened_cb) |
| : role_(Role::kUnassigned), |
| channel_opened_cb_(std::move(channel_opened_cb)), |
| dispatcher_(async_get_default_dispatcher()), |
| initial_param_negotiation_state_( |
| ParameterNegotiationState::kNotNegotiated), |
| weak_ptr_factory_(this) {} |
| |
| bool Session::SetL2CAPChannel(fbl::RefPtr<l2cap::Channel> l2cap_channel) { |
| ZX_DEBUG_ASSERT(!l2cap_channel_); |
| ZX_DEBUG_ASSERT(l2cap_channel); |
| l2cap_channel_.Reset(l2cap_channel); |
| auto self = weak_ptr_factory_.GetWeakPtr(); |
| return l2cap_channel_->Activate( |
| [self](const auto& sdu) { |
| if (self) |
| self->RxCallback(sdu); |
| }, |
| [self]() { |
| if (self) |
| self->ClosedCallback(); |
| }, |
| dispatcher_); |
| } |
| |
| void Session::OpenRemoteChannel(ServerChannel server_channel, |
| ChannelOpenedCallback channel_opened_cb) { |
| if (!multiplexer_started()) { |
| tasks_pending_mux_startup_.emplace( |
| [this, server_channel, cb = std::move(channel_opened_cb)]() mutable { |
| OpenRemoteChannel(server_channel, std::move(cb)); |
| }); |
| if (role_ == Role::kUnassigned) |
| StartupMultiplexer(); |
| return; |
| } |
| |
| // RFCOMM 5.4: An RFCOMM entity making a new DLC on an existing session forms |
| // the DLCI by combining the Server Channel for the application on the other |
| // device, and the inverse of its own direction bit for the session." |
| DLCI dlci = ServerChannelToDLCI(server_channel, OppositeRole(role_)); |
| |
| // Make a new channel (with initial not-negotiated state) if this is the |
| // first we've heard of this DLCI. |
| auto chan_place = FindOrCreateChannel(dlci); |
| if (chan_place.second) { |
| bt_log(TRACE, "rfcomm", "initialized new remote channel %u", dlci); |
| } |
| |
| // If we haven't negotiated initial parameters, queue this and possibly start. |
| if (initial_param_negotiation_state_ != |
| ParameterNegotiationState::kNegotiated) { |
| tasks_pending_parameter_negotiation_.emplace( |
| [this, server_channel, cb = std::move(channel_opened_cb)]() mutable { |
| OpenRemoteChannel(server_channel, std::move(cb)); |
| }); |
| |
| // If it's not started, start it |
| if (initial_param_negotiation_state_ == |
| ParameterNegotiationState::kNotNegotiated) |
| RunInitialParameterNegotiation(dlci); |
| |
| return; |
| } |
| |
| SendCommand( |
| FrameType::kSetAsynchronousBalancedMode, dlci, |
| [this, dlci, server_channel, cb = std::move(channel_opened_cb)]( |
| std::unique_ptr<Frame> response) mutable { |
| ZX_DEBUG_ASSERT(response); |
| FrameType type = FrameType(response->control()); |
| fbl::RefPtr<Channel> new_channel; |
| switch (type) { |
| case FrameType::kUnnumberedAcknowledgement: { |
| bt_log(TRACE, "rfcomm", "channel %u started successfully", dlci); |
| new_channel = GetChannel(dlci); |
| ZX_DEBUG_ASSERT(new_channel); |
| new_channel->established_ = true; |
| break; |
| } |
| case FrameType::kDisconnectedMode: |
| bt_log(TRACE, "rfcomm", "channel %u failed to start", dlci); |
| channels_.erase(dlci); |
| break; |
| default: |
| bt_log(WARN, "rfcomm", "unexpected response to SABM: %u", |
| static_cast<unsigned>(type)); |
| channels_.erase(dlci); |
| break; |
| } |
| // Send the result. |
| async::PostTask(dispatcher_, |
| [server_channel, new_channel, cb_ = std::move(cb)] { |
| cb_(new_channel, server_channel); |
| }); |
| }); |
| } |
| |
| void Session::RxCallback(const l2cap::SDU& sdu) { |
| l2cap::PDU::Reader reader(&sdu); |
| reader.ReadNext(sdu.length(), [&](const common::ByteBuffer& buffer) { |
| auto frame = Frame::Parse(credit_based_flow_, OppositeRole(role_), buffer); |
| if (!frame) { |
| bt_log(ERROR, "rfcomm", "could not parse frame"); |
| return; |
| } |
| |
| DLCI dlci = frame->dlci(); |
| |
| switch ((FrameType)frame->control()) { |
| case FrameType::kSetAsynchronousBalancedMode: |
| HandleSABM(dlci); |
| return; |
| |
| case FrameType::kUnnumberedAcknowledgement: |
| case FrameType::kDisconnectedMode: { |
| auto callbacks_it = outstanding_frames_.find(dlci); |
| if (callbacks_it == outstanding_frames_.end()) { |
| bt_log(TRACE, "rfcomm", "unsolicited UA or DM frame"); |
| return; |
| } |
| |
| // Cancel the timeout and run the callback. |
| callbacks_it->second.second->Cancel(); |
| async::PostTask( |
| dispatcher_, |
| [cb = std::move(callbacks_it->second.first), |
| fr = std::move(frame)]() mutable { cb(std::move(fr)); }); |
| |
| outstanding_frames_.erase(dlci); |
| |
| return; |
| } |
| |
| case FrameType::kUnnumberedInfoHeaderCheck: { |
| if (dlci == kMuxControlDLCI) { |
| HandleMuxCommand(frame->AsMuxCommandFrame()->TakeMuxCommand()); |
| return; |
| } else if (IsUserDLCI(dlci)) { |
| auto chan = GetChannel(dlci); |
| if (!chan) { |
| bt_log(WARN, "rfcomm", "data received for unopened DLCI %u", dlci); |
| return; |
| } |
| |
| auto* data_frame = frame->AsUserDataFrame(); |
| // TODO(gusss): Currently, our UIH frames can bear credits, but it |
| // only makes sense for UserDataFrames to bear credits (because you |
| // don't send credits along the mux control channel). Should change |
| // this in frames.[h,cc]. |
| if (credit_based_flow_) { |
| HandleReceivedCredits(dlci, data_frame->credits()); |
| if (CreditsApply(*frame)) { |
| // Reduce their number of credits if it should be reduced. |
| if (frame->length() > 0) { |
| chan->remote_credits_--; |
| } |
| |
| if (chan->remote_credits_ <= kLowWaterMark) { |
| // Replenish the remote's credits by sending an empty user data |
| // frame, which will attach the max amount of credits. |
| SendUserData(dlci, nullptr); |
| } |
| } |
| } |
| |
| chan->Receive(data_frame->TakeInformation()); |
| return; |
| } else { |
| bt_log(WARN, "rfcomm", "UIH frame on invalid DLCI %u", dlci); |
| } |
| } |
| |
| default: |
| // TODO(gusss): implement better error handling here. |
| bt_log(WARN, "rfcomm", "unrecognized frame type received: %u", |
| frame->control()); |
| return; |
| } |
| }); |
| } |
| |
| void Session::ClosedCallback() { Closedown(); } |
| |
| void Session::SendCommand(FrameType frame_type, DLCI dlci, |
| CommandResponseCallback command_response_cb) { |
| ZX_DEBUG_ASSERT(frame_type == FrameType::kSetAsynchronousBalancedMode || |
| frame_type == FrameType::kDisconnect); |
| ZX_DEBUG_ASSERT(IsValidDLCI(dlci)); |
| ZX_DEBUG_ASSERT_MSG( |
| outstanding_frames_.find(dlci) == outstanding_frames_.end(), |
| "there is already an outstanding command frame for DLCI %u", dlci); |
| |
| auto timeout_cb = std::make_unique<async::TaskClosure>([this, dlci] { |
| bt_log(ERROR, "rfcomm", |
| "outstanding frame on DLCI %u timed out; closing down session", |
| dlci); |
| Closedown(); |
| }); |
| |
| // Set response and timeout callbacks. |
| auto callbacks = |
| std::make_pair(std::move(command_response_cb), std::move(timeout_cb)); |
| outstanding_frames_.emplace(dlci, std::move(callbacks)); |
| |
| // A different timeout is used if this is a SABM command on a user data |
| // channel (RFCOMM 5.3). |
| zx::duration timeout = |
| (frame_type == FrameType::kSetAsynchronousBalancedMode && |
| IsUserDLCI(dlci)) |
| ? kAcknowledgementTimerUserDLCs |
| : kAcknowledgementTimer; |
| |
| std::unique_ptr<Frame> frame; |
| if (frame_type == FrameType::kSetAsynchronousBalancedMode) { |
| frame = std::make_unique<SetAsynchronousBalancedModeCommand>(role_, dlci); |
| } else { |
| frame = std::make_unique<DisconnectCommand>(role_, dlci); |
| } |
| |
| bool sent = SendFrame(std::move(frame), [this, timeout, dlci] { |
| outstanding_frames_[dlci].second->PostDelayed(dispatcher_, timeout); |
| }); |
| ZX_DEBUG_ASSERT(sent); |
| } |
| |
| void Session::SendResponse(FrameType frame_type, DLCI dlci) { |
| ZX_DEBUG_ASSERT(frame_type == FrameType::kUnnumberedAcknowledgement || |
| frame_type == FrameType::kDisconnectedMode); |
| ZX_DEBUG_ASSERT(IsValidDLCI(dlci)); |
| |
| std::unique_ptr<Frame> frame; |
| if (frame_type == FrameType::kUnnumberedAcknowledgement) |
| frame = std::make_unique<UnnumberedAcknowledgementResponse>(role_, dlci); |
| else |
| frame = std::make_unique<DisconnectedModeResponse>(role_, dlci); |
| |
| bool sent = SendFrame(std::move(frame)); |
| ZX_DEBUG_ASSERT(sent); |
| } |
| |
| bool Session::SendFrame(std::unique_ptr<Frame> frame, fit::closure sent_cb) { |
| ZX_DEBUG_ASSERT(frame); |
| const DLCI dlci = frame->dlci(); |
| const FrameType type = static_cast<FrameType>(frame->control()); |
| |
| auto chan = GetChannel(dlci); |
| ZX_DEBUG_ASSERT(chan); |
| |
| // If the multiplexer isn't started, only startup frames should be sent. |
| ZX_DEBUG_ASSERT(multiplexer_started() || IsMuxStartupFrame(type, dlci)); |
| |
| // TODO(gusss): check that the DLC is actually open. |
| |
| // TODO(NET-1079, NET-1080): check flow control and queue the frame if it |
| // needs to be queued. |
| |
| // Checking that we have enough credits to send this frame. |
| if (credit_based_flow_ && CreditsApply(*frame)) { |
| if (chan->local_credits_ <= 0) { |
| QueueFrame(std::move(frame), std::move(sent_cb)); |
| return true; |
| } |
| } |
| |
| // The number of credits which will be attached to this frame. |
| FrameCredits replenish_credits = 0; |
| if (credit_based_flow_ && CreditsCandidate(frame.get())) { |
| replenish_credits = kHighWaterMark - chan->remote_credits_; |
| frame->AsUnnumberedInfoHeaderCheckFrame()->set_credits(replenish_credits); |
| } |
| |
| // Allocate and write the buffer. |
| auto buffer = common::NewSlabBuffer(frame->written_size()); |
| if (!buffer) { |
| bt_log(WARN, "rfcomm", "couldn't allocate frame buffer (%zu)", |
| frame->written_size()); |
| return false; |
| } |
| frame->Write(buffer->mutable_view()); |
| |
| if (l2cap_channel_->Send(std::move(buffer))) { |
| if (credit_based_flow_) { |
| // If the frame had attached credits, increment remote credits. |
| if (CreditsCandidate(frame.get())) { |
| chan->remote_credits_ += replenish_credits; |
| } |
| |
| // If this frame itself required a credit to send, reduce our number of |
| // credits. |
| if (CreditsApply(*frame)) { |
| chan->local_credits_--; |
| } |
| } |
| |
| if (sent_cb) |
| sent_cb(); |
| return true; |
| } else { |
| bt_log(ERROR, "rfcomm", "failed to send frame"); |
| return false; |
| } |
| } |
| |
| void Session::SendMuxCommand(std::unique_ptr<MuxCommand> mux_command, |
| MuxResponseCallback callback) { |
| ZX_DEBUG_ASSERT(mux_command); |
| |
| // If we're not expecting a response, we can send right away. |
| if (!callback) { |
| auto frame = std::make_unique<MuxCommandFrame>(role_, credit_based_flow_, |
| std::move(mux_command)); |
| bool sent = SendFrame(std::move(frame)); |
| ZX_DEBUG_ASSERT(sent); |
| return; |
| } |
| |
| // If we're expecting a response at the mulitplexer level, store the callback |
| // that should be called when the response arrives. |
| MuxCommandType type = mux_command->command_type(); |
| DLCI dlci = GetDLCIFromMuxCommand(mux_command.get()); |
| OutstandingMuxCommand key = std::make_pair(type, dlci); |
| |
| ZX_DEBUG_ASSERT_MSG( |
| outstanding_mux_commands_.find(key) == outstanding_mux_commands_.end(), |
| "already an outstanding mux command for (command type: %u, dlci: %u)", |
| static_cast<unsigned int>(type), dlci); |
| |
| auto timeout_cb = |
| std::make_unique<async::TaskClosure>([this] { Closedown(); }); |
| |
| // Set response and timeout callbacks. |
| outstanding_mux_commands_.emplace( |
| key, std::make_pair(std::move(callback), std::move(timeout_cb))); |
| |
| auto frame = std::make_unique<MuxCommandFrame>(role_, credit_based_flow_, |
| std::move(mux_command)); |
| |
| bool sent = SendFrame(std::move(frame), [this, key] { |
| outstanding_mux_commands_[key].second->PostDelayed(dispatcher_, |
| kMuxResponseTimer); |
| }); |
| ZX_DEBUG_ASSERT(sent); |
| } |
| |
| void Session::StartupMultiplexer() { |
| if (role_ == Role::kNegotiating || multiplexer_started()) { |
| bt_log(WARN, "rfcomm", "=StartupMultiplexer when starting or started"); |
| return; |
| } |
| |
| bt_log(TRACE, "rfcomm", "starting multiplexer"); |
| FindOrCreateChannel(kMuxControlDLCI); |
| |
| role_ = Role::kNegotiating; |
| |
| SendCommand(FrameType::kSetAsynchronousBalancedMode, kMuxControlDLCI, |
| [this](auto response) { |
| ZX_DEBUG_ASSERT(response); |
| |
| FrameType type = static_cast<FrameType>(response->control()); |
| ZX_DEBUG_ASSERT(type == FrameType::kUnnumberedAcknowledgement || |
| type == FrameType::kDisconnectedMode); |
| |
| switch (role_) { |
| case Role::kNegotiating: { |
| if (type == FrameType::kUnnumberedAcknowledgement) { |
| SetMultiplexerStarted(Role::kInitiator); |
| } else { |
| bt_log(WARN, "rfcomm", |
| "remote multiplexer startup refused by remote"); |
| role_ = Role::kUnassigned; |
| } |
| return; |
| } |
| case Role::kUnassigned: |
| case Role::kInitiator: |
| case Role::kResponder: |
| // TODO(guss): should a UA be received in any of these |
| // cases? |
| bt_log(WARN, "rfcomm", |
| "mux UA frame received in unexpected state"); |
| break; |
| default: |
| // TODO(gusss): shouldn't get here. |
| ZX_PANIC("invalid role: %u", |
| static_cast<unsigned int>(role_)); |
| break; |
| } |
| }); |
| } |
| |
| void Session::HandleSABM(DLCI dlci) { |
| if (dlci == kMuxControlDLCI) { |
| // A SABM frame on the mux control DLCI indicates that we should start |
| // up the multiplexer. |
| switch (role_) { |
| case Role::kUnassigned: { |
| // We received a SABM request to start the multiplexer. Reply positively |
| // to the request, meaning that the peer becomes the initiator and this |
| // session becomes the responder, and initialize our MuxCommand channel. |
| FindOrCreateChannel(kMuxControlDLCI); |
| SendResponse(FrameType::kUnnumberedAcknowledgement, dlci); |
| SetMultiplexerStarted(Role::kResponder); |
| return; |
| } |
| case Role::kNegotiating: { |
| // In this case, we have an outstanding request to start the |
| // multiplexer. Respond negatively and attempt startup again later. See |
| // RFCOMM 5.2.1. |
| |
| bt_log(TRACE, "rfcomm", "resolving multiplexer startup conflict"); |
| |
| // "Undo" our multiplexer startup request by changing our role back, |
| // cancelling timeout, and removing callbacks. |
| role_ = Role::kUnassigned; |
| auto frame_it = outstanding_frames_.find(dlci); |
| ZX_DEBUG_ASSERT(frame_it != outstanding_frames_.end()); |
| frame_it->second.second->Cancel(); |
| outstanding_frames_.erase(frame_it); |
| |
| SendResponse(FrameType::kDisconnectedMode, kMuxControlDLCI); |
| async::PostDelayedTask( |
| dispatcher_, |
| [this] { |
| if (!multiplexer_started()) { |
| bt_log(TRACE, "rfcomm", "retrying multiplexer startup"); |
| StartupMultiplexer(); |
| } |
| }, |
| kMuxStartupConflictDelay); |
| return; |
| } |
| case Role::kInitiator: |
| case Role::kResponder: |
| // TODO(gusss): should we send a DM in this case? |
| bt_log(WARN, "rfcomm", "request to start already started multiplexer"); |
| return; |
| default: |
| ZX_PANIC("invalid role: %u", static_cast<unsigned int>(role_)); |
| return; |
| } |
| } |
| |
| // If it isn't a multiplexer startup request, it must be a request for a user |
| // channel. |
| |
| // TODO(NET-1301): unit test this case. |
| if (!IsUserDLCI(dlci) || !IsValidLocalChannel(role_, dlci)) { |
| bt_log(WARN, "rfcomm", "remote requested invalid DLCI %u", dlci); |
| SendResponse(FrameType::kDisconnectedMode, dlci); |
| return; |
| } |
| |
| // TODO(NET-1301): unit test this case. |
| // Initialize the channel if we don't already know about it, to respond to |
| // negotiation appropriately. |
| auto chan_placed = FindOrCreateChannel(dlci); |
| auto channel = std::move(chan_placed.first); |
| // If the channel already exists, we must have just negotiated it, without |
| // having it be established. |
| if (channel->established_) { |
| // If the channel is already open, the remote is confused about the state of |
| // the Session. Send a DM and a DISC for that channel. |
| // TODO(NET-1274): do we want to just shut down the whole session here? |
| // Things would be in a nasty state at this point. |
| bt_log(TRACE, "rfcomm", "remote requested already open channel"); |
| SendResponse(FrameType::kDisconnectedMode, dlci); |
| SendCommand(FrameType::kDisconnect, dlci, [](auto response) { |
| // TODO(NET-1273): implement clean channel close + state reset |
| }); |
| channels_.erase(dlci); |
| return; |
| } |
| |
| // Start the channel by first responding positively. |
| SendResponse(FrameType::kUnnumberedAcknowledgement, dlci); |
| channel->established_ = true; |
| |
| async::PostTask(dispatcher_, [this, dlci, channel] { |
| channel_opened_cb_(channel, DLCIToServerChannel(dlci)); |
| }); |
| |
| bt_log(INFO, "rfcomm", "remote peer opened channel with DLCI %u", dlci); |
| } |
| |
| void Session::HandleMuxCommand(std::unique_ptr<MuxCommand> mux_command) { |
| ZX_DEBUG_ASSERT(mux_command); |
| MuxCommandType type = mux_command->command_type(); |
| |
| if (mux_command->command_response() == CommandResponse::kResponse) { |
| auto dlci = GetDLCIFromMuxCommand(mux_command.get()); |
| auto key = std::make_pair(type, dlci); |
| |
| auto command_it = outstanding_mux_commands_.find(key); |
| if (command_it == outstanding_mux_commands_.end()) { |
| bt_log(WARN, "rfcomm", |
| "got response but no outstanding command for (type: %u, DLCI: %u)", |
| static_cast<unsigned>(type), dlci); |
| return; |
| } |
| |
| // Cancel the timeout and call the callback. |
| command_it->second.second->Cancel(); |
| async::PostTask( |
| dispatcher_, |
| [cmd = std::move(mux_command), |
| cb = std::move(outstanding_mux_commands_[key].first)]() mutable { |
| cb(std::move(cmd)); |
| }); |
| |
| outstanding_mux_commands_.erase(command_it); |
| |
| return; |
| } |
| |
| // Otherwise, it's a command. |
| switch (type) { |
| case MuxCommandType::kDLCParameterNegotiation: { |
| auto pn_command = std::unique_ptr<DLCParameterNegotiationCommand>( |
| static_cast<DLCParameterNegotiationCommand*>(mux_command.release())); |
| ParameterNegotiationParams received_params = pn_command->params(); |
| |
| if (!IsUserDLCI(received_params.dlci)) { |
| bt_log(WARN, "rfcomm", |
| "received parameter negotiation command for invalid DLCI %u", |
| received_params.dlci); |
| SendResponse(FrameType::kDisconnectedMode, received_params.dlci); |
| return; |
| } |
| |
| auto chan = GetChannel(received_params.dlci); |
| if (chan) { |
| if (chan->negotiation_state_ != |
| ParameterNegotiationState::kNotNegotiated) { |
| // RFCOMM 5.5.3 states that supporting re-negotiation of DLCIs is |
| // optional, and we can just reply with our own parameters. |
| bt_log(TRACE, "rfcomm", "negotiate already-negotiated DLCI %u", |
| received_params.dlci); |
| ParameterNegotiationParams our_params = |
| GetIdealParameters(received_params.dlci); |
| our_params.credit_based_flow_handshake = |
| CreditBasedFlowHandshake::kSupportedResponse; |
| our_params.maximum_frame_size = maximum_frame_size_; |
| SendMuxCommand(std::make_unique<DLCParameterNegotiationCommand>( |
| CommandResponse::kResponse, our_params)); |
| return; |
| } |
| |
| if (initial_param_negotiation_state_ == |
| ParameterNegotiationState::kNegotiated && |
| received_params.maximum_frame_size != maximum_frame_size_) { |
| // RFCOMM 5.5.3 states that a responder may issue a DM frame if they |
| // are unwilling to establish a connection. In this case, we use it to |
| // reject any non-initial PN command which attempts to change the |
| // maximum frame size. |
| bt_log(TRACE, "rfcomm", |
| "peer requested different max frame size after initial " |
| "negotiation; rejecting"); |
| SendResponse(FrameType::kDisconnectedMode, received_params.dlci); |
| } |
| return; |
| } |
| |
| // Unknown channel being negotiated. Initialize it. |
| auto chan_place = FindOrCreateChannel(received_params.dlci); |
| ZX_DEBUG_ASSERT(chan_place.second); |
| chan = std::move(chan_place.first); |
| bt_log(TRACE, "rfcomm", "initialized DLCI %d for parameter negotiation", |
| received_params.dlci); |
| |
| ParameterNegotiationParams ideal_params = |
| GetIdealParameters(received_params.dlci); |
| |
| // Take the credits they give us, and log the credits we give them. |
| HandleReceivedCredits(received_params.dlci, |
| received_params.initial_credits); |
| chan->remote_credits_ = ideal_params.initial_credits; |
| |
| // Parameter negotiation described in GSM 5.4.6.3.1 (under table 5). |
| ParameterNegotiationParams negotiated_params; |
| // DLCI does not change. |
| negotiated_params.dlci = received_params.dlci; |
| // Respond with a positive credit-based flow handshake response iff we |
| // received a positive request. |
| negotiated_params.credit_based_flow_handshake = |
| received_params.credit_based_flow_handshake == |
| CreditBasedFlowHandshake::kSupportedRequest |
| ? CreditBasedFlowHandshake::kSupportedResponse |
| : CreditBasedFlowHandshake::kUnsupported; |
| // Priority does not change. |
| negotiated_params.priority = received_params.priority; |
| |
| // Accept their max frame size if this is the initial negotiation and if |
| // it's less than or equal to ours; otherwise, use ours. |
| if (initial_param_negotiation_state_ != |
| ParameterNegotiationState::kNegotiated) { |
| negotiated_params.maximum_frame_size = |
| received_params.maximum_frame_size <= |
| ideal_params.maximum_frame_size |
| ? received_params.maximum_frame_size |
| : ideal_params.maximum_frame_size; |
| } else { |
| ZX_DEBUG_ASSERT(received_params.maximum_frame_size == |
| maximum_frame_size_); |
| negotiated_params.maximum_frame_size = maximum_frame_size_; |
| } |
| |
| negotiated_params.initial_credits = ideal_params.initial_credits; |
| |
| // Update settings. |
| |
| if (initial_param_negotiation_state_ != |
| ParameterNegotiationState::kNegotiated) { |
| // Set credit-based flow and max frame size only on initial PN. |
| credit_based_flow_ = received_params.credit_based_flow_handshake == |
| CreditBasedFlowHandshake::kSupportedRequest; |
| maximum_frame_size_ = negotiated_params.maximum_frame_size; |
| InitialParameterNegotiationComplete(); |
| } |
| |
| // TODO(NET-1130): set priority if/when priority is implemented. |
| |
| bt_log(TRACE, "rfcomm", |
| "parameters negotiated: DLCI %u, credit-based flow %s, " |
| "(ours %u, theirs %u), priority %u, max frame size %u", |
| negotiated_params.dlci, |
| (negotiated_params.credit_based_flow_handshake == |
| CreditBasedFlowHandshake::kSupportedResponse |
| ? "on" |
| : "off"), |
| received_params.initial_credits, |
| negotiated_params.initial_credits, negotiated_params.priority, |
| negotiated_params.maximum_frame_size); |
| |
| // Respond with the negotiated params. |
| SendMuxCommand(std::make_unique<DLCParameterNegotiationCommand>( |
| CommandResponse::kResponse, negotiated_params)); |
| |
| chan->negotiation_state_ = ParameterNegotiationState::kNegotiated; |
| return; |
| } |
| default: { |
| bt_log(ERROR, "rfcomm", "unsupported mux command: %u", |
| static_cast<unsigned int>(type)); |
| break; |
| } |
| } |
| } |
| |
| void Session::SetMultiplexerStarted(Role role) { |
| ZX_DEBUG_ASSERT(role == Role::kInitiator || role == Role::kResponder); |
| |
| role_ = role; |
| bt_log(TRACE, "rfcomm", "multiplexer started (role: %s)", |
| role == Role::kInitiator ? "initiator" : "responder"); |
| |
| // Run any pending tasks. |
| while (!tasks_pending_mux_startup_.empty()) { |
| async::PostTask(dispatcher_, std::move(tasks_pending_mux_startup_.front())); |
| tasks_pending_mux_startup_.pop(); |
| } |
| |
| // TODO(gusss): send frames from queue when queueing implemented |
| } |
| |
| void Session::Closedown() { |
| bt_log(TRACE, "rfcomm", "closing session"); |
| // Deactivates the channel. |
| l2cap_channel_ = nullptr; |
| } |
| |
| void Session::RunInitialParameterNegotiation(DLCI dlci) { |
| ZX_DEBUG_ASSERT_MSG( |
| multiplexer_started(), |
| "parameter negotiation requested before multiplexer started"); |
| ZX_DEBUG_ASSERT_MSG(initial_param_negotiation_state_ == |
| ParameterNegotiationState::kNotNegotiated, |
| "initial parameter negotiation already run"); |
| |
| auto chan = GetChannel(dlci); |
| ZX_DEBUG_ASSERT(chan); |
| |
| // Mark the DLCI as in the negotiating state. |
| chan->negotiation_state_ = ParameterNegotiationState::kNegotiating; |
| initial_param_negotiation_state_ = ParameterNegotiationState::kNegotiating; |
| |
| auto params = GetIdealParameters(dlci); |
| |
| auto pn_command = std::make_unique<DLCParameterNegotiationCommand>( |
| CommandResponse::kCommand, params); |
| SendMuxCommand(std::move(pn_command), [this, dlci, priority = params.priority, |
| maximum_frame_size = |
| params.maximum_frame_size, |
| their_credits = params.initial_credits, |
| chan = std::move(chan)]( |
| auto mux_command) { |
| ZX_DEBUG_ASSERT(initial_param_negotiation_state_ == |
| ParameterNegotiationState::kNegotiating || |
| initial_param_negotiation_state_ == |
| ParameterNegotiationState::kNegotiated); |
| |
| // If we fail negotation for any reason, remove the nascent channel and |
| // reset to not-negotiated if negotiation didn't already finish. |
| auto failed_negotiation = fit::defer([this, dlci] { |
| channels_.erase(dlci); |
| if (initial_param_negotiation_state_ == |
| ParameterNegotiationState::kNegotiating) { |
| initial_param_negotiation_state_ = |
| ParameterNegotiationState::kNotNegotiated; |
| } |
| }); |
| |
| if (mux_command == nullptr) { |
| // A response of nullptr signals a DM response from the peer. |
| bt_log(TRACE, "rfcomm", "PN command for DLCI %u rejected", dlci); |
| return; |
| } |
| |
| ZX_DEBUG_ASSERT(mux_command->command_type() == |
| MuxCommandType::kDLCParameterNegotiation && |
| mux_command->command_response() == |
| CommandResponse::kResponse); |
| |
| auto pn_response = std::unique_ptr<DLCParameterNegotiationCommand>( |
| static_cast<DLCParameterNegotiationCommand*>(mux_command.release())); |
| auto params = pn_response->params(); |
| |
| if (dlci != params.dlci) { |
| bt_log(TRACE, "rfcomm", "remote changed DLCI in PN response"); |
| SendCommand(FrameType::kDisconnect, dlci); |
| // don't need to erase params.dlci, it's unknown. |
| return; |
| } |
| |
| // TODO(gusss): currently we completely ignore priority (other than this |
| // check) |
| if (params.priority != priority) |
| bt_log(TRACE, "rfcomm", "remote changed priority in PN response"); |
| |
| if (params.maximum_frame_size > maximum_frame_size) { |
| bt_log(WARN, "rfcomm", |
| "peer's PN response contained an invalid max frame size"); |
| SendCommand(FrameType::kDisconnect, dlci); |
| return; |
| } |
| |
| if (initial_param_negotiation_state_ == |
| ParameterNegotiationState::kNegotiated && |
| params.maximum_frame_size != maximum_frame_size_) { |
| bt_log(WARN, "rfcomm", |
| "peer tried to change max frame size after initial" |
| " param negotiation; rejecting"); |
| SendCommand(FrameType::kDisconnect, dlci); |
| return; |
| } |
| |
| // Successfully negotiated. |
| failed_negotiation.cancel(); |
| |
| // Only set these parameters on initial parameter negotiation. |
| if (initial_param_negotiation_state_ == |
| ParameterNegotiationState::kNegotiating) { |
| // Credit-based flow is turned on if the peer sends the correct |
| // response. |
| credit_based_flow_ = params.credit_based_flow_handshake == |
| CreditBasedFlowHandshake::kSupportedResponse; |
| |
| maximum_frame_size_ = params.maximum_frame_size; |
| InitialParameterNegotiationComplete(); |
| } |
| |
| // Take the credits they give us, and log the credits we give them. |
| HandleReceivedCredits(dlci, params.initial_credits); |
| chan->remote_credits_ = their_credits; |
| |
| bt_log(TRACE, "rfcomm", |
| "parameters negotiated: DLCI %u, credit-based flow %s " |
| "(ours %u, theirs %u), priority %u, max frame size %u", |
| params.dlci, (credit_based_flow_ ? "on" : "off"), |
| params.initial_credits, their_credits, params.priority, |
| maximum_frame_size_); |
| |
| // Set channel to not negotiating anymore. |
| chan->negotiation_state_ = ParameterNegotiationState::kNegotiated; |
| }); |
| } |
| |
| ParameterNegotiationParams Session::GetIdealParameters(DLCI dlci) const { |
| ZX_DEBUG_ASSERT(IsValidDLCI(dlci)); |
| |
| // We set the MTU of the RFCOMM channel based on the MTUs of the underlying |
| // L2CAP link; we take the minimum of the two. |
| uint16_t maximum_frame_size = |
| (l2cap_channel_->rx_mtu() < l2cap_channel_->tx_mtu() |
| ? l2cap_channel_->rx_mtu() |
| : l2cap_channel_->tx_mtu()); |
| |
| // GSM Table 27. |
| Priority priority = 61; |
| if (dlci == kMuxControlDLCI) { |
| priority = 0; |
| } else if (dlci <= 7) { |
| priority = 7; |
| } else if (dlci <= 15) { |
| priority = 15; |
| } else if (dlci <= 23) { |
| priority = 23; |
| } else if (dlci <= 31) { |
| priority = 31; |
| } else if (dlci <= 39) { |
| priority = 39; |
| } else if (dlci <= 47) { |
| priority = 47; |
| } else if (dlci <= 55) { |
| priority = 55; |
| } |
| |
| return {dlci, |
| // We always attempt to enable credit-based flow (RFCOMM 5.5.3). |
| CreditBasedFlowHandshake::kSupportedRequest, priority, |
| maximum_frame_size, kMaxInitialCredits}; |
| } |
| |
| void Session::InitialParameterNegotiationComplete() { |
| bt_log(TRACE, "rfcomm", "initial parameter negotiation complete"); |
| |
| initial_param_negotiation_state_ = ParameterNegotiationState::kNegotiated; |
| |
| // TODO(gusss): need to test this in unittests. try starting multiple channels |
| // before completing parameter negotiation. |
| while (!tasks_pending_parameter_negotiation_.empty()) { |
| async::PostTask(dispatcher_, |
| std::move(tasks_pending_parameter_negotiation_.front())); |
| tasks_pending_parameter_negotiation_.pop(); |
| } |
| } |
| |
| void Session::QueueFrame(std::unique_ptr<Frame> frame, fit::closure sent_cb) { |
| auto chan = GetChannel(frame->dlci()); |
| ZX_DEBUG_ASSERT(chan); |
| chan->wait_queue_.push(std::make_pair(std::move(frame), std::move(sent_cb))); |
| } |
| |
| void Session::TrySendQueued() { |
| for (const auto& it : channels_) { |
| TrySendQueued(it.first); |
| } |
| } |
| |
| void Session::TrySendQueued(DLCI dlci) { |
| ZX_DEBUG_ASSERT(dlci == kMuxControlDLCI || IsUserDLCI(dlci)); |
| auto chan = GetChannel(dlci); |
| if (!chan) { |
| return; |
| } |
| auto& queue = chan->wait_queue_; |
| |
| // We attempt to send each frame in the queue. Note that some frames might get |
| // added back to the queue, while others may not (e.g. we might successfully |
| // send a multiplexer command, but a user data frame might get queued due to |
| // credit-based flow control). |
| size_t num_to_send = queue.size(); |
| while (num_to_send--) { |
| auto& frame_and_cb = queue.front(); |
| bool sent = SendFrame(std::move(frame_and_cb.first), |
| std::move(frame_and_cb.second)); |
| ZX_DEBUG_ASSERT(sent); |
| queue.pop(); |
| } |
| } |
| |
| // Gets the Channel for |dlci|, or a null pointer if it doesn't exist. |
| fbl::RefPtr<Channel> Session::GetChannel(DLCI dlci) { |
| auto it = channels_.find(dlci); |
| if (it == channels_.end()) { |
| return nullptr; |
| } |
| return it->second; |
| } |
| |
| // Finds or iniitalizes a new Channel object for |dlci| |
| // Returns a pair with the channel and a boolean indicating if it was created. |
| std::pair<fbl::RefPtr<Channel>, bool> Session::FindOrCreateChannel(DLCI dlci) { |
| auto chan_place = channels_.emplace( |
| dlci, fbl::AdoptRef(new internal::ChannelImpl(dlci, this))); |
| return std::make_pair(chan_place.first->second, chan_place.second); |
| } |
| |
| void Session::HandleReceivedCredits(DLCI dlci, FrameCredits credits) { |
| auto chan = GetChannel(dlci); |
| ZX_DEBUG_ASSERT(chan); |
| |
| bool credits_were_zero = chan->local_credits_ == 0; |
| chan->local_credits_ += credits; |
| |
| // If we went from zero to nonzero credits, attempt to send frames. |
| if (credits_were_zero && credits) |
| TrySendQueued(); |
| } |
| |
| } // namespace rfcomm |
| } // namespace btlib |