// Copyright 2023 Google LLC // // 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 // // http://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. /// L2CAP CoC server bridge: waits for a peer to connect an L2CAP CoC channel /// on a specified PSM. When the connection is made, the bridge connects a TCP /// socket to a remote host and bridges the data in both directions, with flow /// control. /// When the L2CAP CoC channel is closed, the bridge disconnects the TCP socket /// and waits for a new L2CAP CoC channel to be connected. /// When the TCP connection is closed by the TCP server, the L2CAP connection is closed as well. use crate::cli::l2cap::{proxy_l2cap_rx_to_tcp_tx, proxy_tcp_rx_to_l2cap_tx, BridgeData}; use bumble::wrapper::{device::Device, hci::HciConstant, l2cap::LeConnectionOrientedChannel}; use futures::executor::block_on; use owo_colors::OwoColorize; use pyo3::{PyResult, Python}; use std::{sync::Arc, time::Duration}; use tokio::{ join, net::TcpStream, select, sync::{mpsc, Mutex}, }; pub struct Args { pub psm: u16, pub max_credits: Option, pub mtu: Option, pub mps: Option, pub tcp_host: String, pub tcp_port: u16, } pub async fn start(args: &Args, device: &mut Device) -> PyResult<()> { let host = args.tcp_host.clone(); let port = args.tcp_port; device.register_l2cap_channel_server( args.psm, move |py, l2cap_channel| { let channel_info = l2cap_channel .debug_string() .unwrap_or_else(|e| format!("failed to get l2cap channel info ({e})")); println!("{} {channel_info}", "*** L2CAP channel:".cyan()); let host = host.clone(); // Handles setting up a tokio runtime that runs this future to completion while also // containing the necessary context vars. pyo3_asyncio::tokio::future_into_py( py, proxy_data_between_l2cap_and_tcp(l2cap_channel, host, port), )?; Ok(()) }, args.max_credits, args.mtu, args.mps, )?; println!( "{}", format!("### Listening for CoC connection on PSM {}", args.psm).yellow() ); device.on_connection(|_py, mut connection| { let connection_info = connection .debug_string() .unwrap_or_else(|e| format!("failed to get connection info ({e})")); println!( "{} {}", "@@@ Bluetooth connection: ".green(), connection_info, ); connection.on_disconnection(|_py, reason| { let disconnection_info = match HciConstant::error_name(reason) { Ok(info_string) => info_string, Err(py_err) => format!("failed to get disconnection error name ({})", py_err), }; println!( "{} {}", "@@@ Bluetooth disconnection: ".red(), disconnection_info, ); Ok(()) })?; Ok(()) })?; device.start_advertising(false).await?; Ok(()) } async fn proxy_data_between_l2cap_and_tcp( mut l2cap_channel: LeConnectionOrientedChannel, tcp_host: String, tcp_port: u16, ) -> PyResult<()> { let (l2cap_to_tcp_tx, mut l2cap_to_tcp_rx) = mpsc::channel::(10); // Set callback (`set_sink`) for when l2cap data is received. let l2cap_to_tcp_tx_clone = l2cap_to_tcp_tx.clone(); l2cap_channel .set_sink(move |_py, sdu| { block_on(l2cap_to_tcp_tx_clone.send(BridgeData::Data(sdu.into()))) .expect("failed to channel data to tcp"); Ok(()) }) .expect("failed to set sink for l2cap connection"); // Set l2cap callback for when the channel is closed. l2cap_channel .on_close(move |_py| { println!("{}", "*** L2CAP channel closed".red()); block_on(l2cap_to_tcp_tx.send(BridgeData::CloseSignal)) .expect("failed to channel close signal to tcp"); Ok(()) }) .expect("failed to set on_close callback for l2cap channel"); println!( "{}", format!("### Connecting to TCP {tcp_host}:{tcp_port}...").yellow() ); let l2cap_channel = Arc::new(Mutex::new(Some(l2cap_channel))); let tcp_stream = match TcpStream::connect(format!("{tcp_host}:{tcp_port}")).await { Ok(stream) => { println!("{}", "### Connected".green()); Some(stream) } Err(err) => { println!("{}", format!("!!! Connection failed: {err}").red()); if let Some(mut channel) = l2cap_channel.lock().await.take() { // Bumble might enter an invalid state if disconnection request is received from // l2cap client before receiving a disconnection response from the same client, // blocking this async call from returning. // See: https://github.com/google/bumble/issues/257 select! { res = channel.disconnect() => { let _ = res.map_err(|e| eprintln!("Failed to call disconnect on l2cap channel: {e}")); }, _ = tokio::time::sleep(Duration::from_secs(1)) => eprintln!("Timed out while calling disconnect on l2cap channel."), } } None } }; match tcp_stream { None => { while let Some(bridge_data) = l2cap_to_tcp_rx.recv().await { match bridge_data { BridgeData::Data(sdu) => { println!("{}", format!("<<< [L2CAP SDU]: {} bytes", sdu.len()).cyan()); println!("{}", "!!! TCP socket not open, dropping".red()) } BridgeData::CloseSignal => break, } } } Some(tcp_stream) => { let (tcp_reader, tcp_writer) = tcp_stream.into_split(); // Do tcp stuff when something happens on the l2cap channel. let handle_l2cap_data_future = proxy_l2cap_rx_to_tcp_tx(l2cap_to_tcp_rx, tcp_writer, l2cap_channel.clone()); // Do l2cap stuff when something happens on tcp. let handle_tcp_data_future = proxy_tcp_rx_to_l2cap_tx(tcp_reader, l2cap_channel.clone(), false); let (handle_l2cap_result, handle_tcp_result) = join!(handle_l2cap_data_future, handle_tcp_data_future); if let Err(e) = handle_l2cap_result { println!("!!! Error: {e}"); } if let Err(e) = handle_tcp_result { println!("!!! Error: {e}"); } } }; Python::with_gil(|_| { // Must hold GIL at least once while/after dropping for Python heap object to ensure // de-allocation. drop(l2cap_channel); }); Ok(()) }