| // 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. |
| |
| use { |
| anyhow::{Context as _, Error}, |
| argh::FromArgs, |
| byteorder::{BigEndian, WriteBytesExt}, |
| fidl_fuchsia_bluetooth_snoop::{PacketType, SnoopEvent, SnoopMarker, SnoopPacket}, |
| fuchsia_async as fasync, |
| fuchsia_bluetooth::error::Error as BTError, |
| fuchsia_component::client::connect_to_service, |
| futures::TryStreamExt, |
| std::{fmt, fs::File, io, path::Path}, |
| }; |
| |
| const PCAP_CMD: u8 = 0x01; |
| const PCAP_DATA: u8 = 0x02; |
| const PCAP_EVENT: u8 = 0x04; |
| |
| enum Format { |
| Pcap, |
| Pretty, |
| } |
| |
| impl fmt::Display for Format { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| let format = match self { |
| Format::Pcap => "pcap", |
| Format::Pretty => "pretty", |
| }; |
| write!(f, "{}", format) |
| } |
| } |
| |
| impl std::str::FromStr for Format { |
| type Err = std::convert::Infallible; |
| fn from_str(s: &str) -> Result<Self, Self::Err> { |
| Ok(match s { |
| "pcap" => Format::Pcap, |
| "pretty" => Format::Pretty, |
| _ => { |
| eprintln!("Unrecognized format. Using pcap"); |
| Format::Pcap |
| } |
| }) |
| } |
| } |
| |
| // Format described in https://wiki.wireshark.org/Development/LibpcapFileFormat#Global_Header |
| pub fn pcap_header() -> Vec<u8> { |
| let mut wtr = vec![]; |
| wtr.write_u32::<BigEndian>(0xa1b2c3d4).unwrap(); // Magic number |
| wtr.write_u16::<BigEndian>(2).unwrap(); // Major Version |
| wtr.write_u16::<BigEndian>(4).unwrap(); // Minor Version |
| wtr.write_i32::<BigEndian>(0).unwrap(); // Timezone: GMT |
| wtr.write_u32::<BigEndian>(0).unwrap(); // Sigfigs |
| wtr.write_u32::<BigEndian>(65535).unwrap(); // Max packet length |
| wtr.write_u32::<BigEndian>(201).unwrap(); // Protocol: BLUETOOTH_HCI_H4_WITH_PHDR |
| wtr |
| } |
| |
| // Format described in |
| // https://wiki.wireshark.org/Development/LibpcapFileFormat#Record_.28Packet.29_Header |
| pub fn to_pcap_fmt(pkt: SnoopPacket) -> Vec<u8> { |
| let mut wtr = vec![]; |
| wtr.write_u32::<BigEndian>(pkt.timestamp.seconds as u32).unwrap(); // timestamp seconds |
| let microseconds = pkt.timestamp.subsec_nanos / 1_000 as u32; // timestamp microseconds |
| wtr.write_u32::<BigEndian>(microseconds).unwrap(); |
| // length is len(payload) + 4 octets for is_received + 1 octet for packet type |
| wtr.write_u32::<BigEndian>((pkt.payload.len() + 5) as u32).unwrap(); // number of octets of packet saved |
| wtr.write_u32::<BigEndian>((pkt.original_len + 5) as u32).unwrap(); // actual length of packet |
| wtr.write_u32::<BigEndian>(pkt.is_received as u32).unwrap(); |
| match pkt.type_ { |
| PacketType::Cmd => { |
| wtr.write_u8(PCAP_CMD).unwrap(); |
| } |
| PacketType::Data => { |
| wtr.write_u8(PCAP_DATA).unwrap(); |
| } |
| PacketType::Event => { |
| wtr.write_u8(PCAP_EVENT).unwrap(); |
| } |
| } |
| wtr.extend(&pkt.payload); |
| wtr |
| } |
| |
| // Pretty print packet metadata with hex representation of payload |
| fn to_pretty_fmt(pkt: SnoopPacket) -> String { |
| let payload = |
| pkt.payload.iter().map(|byte| format!("{:x}", byte)).collect::<Vec<String>>().join(""); |
| format!( |
| "{}.{:09}: {:5} {} {}\n", |
| pkt.timestamp.seconds, |
| pkt.timestamp.subsec_nanos, |
| format!("{:?}", pkt.type_), |
| if pkt.is_received { "RX" } else { "TX" }, |
| payload |
| ) |
| } |
| |
| /// Define the command line arguments that the tool accepts. |
| #[derive(FromArgs)] |
| #[argh(description = "Snoop Bluetooth controller packets")] |
| struct Opt { |
| #[argh(switch, short = 'd')] |
| /// dump the available history of snoop packets |
| dump: bool, |
| |
| #[argh(option, short = 'f', default = "Format::Pcap")] |
| /// file format. options: [pcap, pretty]. Defaults to `pcap`. |
| format: Format, |
| |
| #[argh(option, short = 'c')] |
| /// exit after N packets have been recorded |
| count: Option<u64>, |
| |
| #[argh(option)] |
| /// request snoop log for a single device by name. |
| device: Option<String>, |
| |
| #[argh(option, short = 'o')] |
| /// output location. Default: stdout |
| output: Option<String>, |
| |
| #[argh(option, short = 't')] |
| /// truncate packets to N bytes before outputting them |
| truncate: Option<usize>, |
| } |
| |
| /// Construct and print a human friendly message relaying the behavior the tool has been invoked |
| /// to use. |
| fn print_opts(opts: &Opt) { |
| let action = if opts.dump { "Dumping" } else { "Following" }; |
| let device = if let Some(ref device) = opts.device { device } else { "all devices" }; |
| let truncate = if let Some(size) = opts.truncate { |
| format!("Truncating packets to {} bytes. ", size) |
| } else { |
| String::new() |
| }; |
| let count = |
| if opts.count.is_some() { format!("up to {} ", opts.count.unwrap()) } else { "".into() }; |
| eprintln!( |
| "{action} snoop log for \"{device}\". {truncation}Outputting {count}packets to {output}.", |
| action = action, |
| device = device, |
| truncation = truncate, |
| count = count, |
| output = opts.output.as_ref().unwrap_or(&"stdout".to_string()), |
| ); |
| } |
| |
| fn main_res() -> Result<(), Error> { |
| // Parse and transform command line arguments. |
| let opts: Opt = argh::from_env(); |
| |
| print_opts(&opts); |
| |
| let Opt { dump, format, truncate, count, device, output } = opts; |
| |
| let follow = !dump; |
| |
| let mut out = match output { |
| Some(ref s) => { |
| let path = Path::new(s); |
| Box::new(File::create(&path).unwrap()) as Box<dyn io::Write> |
| } |
| None => Box::new(io::stdout()) as Box<dyn io::Write>, |
| }; |
| |
| // create and run the main future |
| let main_future = async { |
| let snoop_svc = connect_to_service::<SnoopMarker>() |
| .context("failed to connect to bluetooth snoop interface")?; |
| let mut evt_stream = snoop_svc.take_event_stream(); |
| match format { |
| Format::Pcap => out.write(pcap_header().as_slice())?, |
| Format::Pretty => 0, |
| }; |
| out.flush()?; |
| |
| // transform device from Option<String> to Option<&str> |
| let dev_str = device.as_ref().map(|d| d.as_str()); |
| |
| // Send request to start receiving snoop packets |
| let status = snoop_svc.start(follow, dev_str).await?; |
| if let Some(e) = status.error { |
| return Err(BTError::from(*e).into()); |
| } |
| |
| // Receive snoop packet events and output them in the requested format. |
| let mut pkt_count = 0; |
| while let Some(evt) = evt_stream.try_next().await.expect("failed to fetch event") { |
| let SnoopEvent::OnPacket { host_device: _, mut packet } = evt; |
| if let Some(size) = truncate { |
| packet.payload.truncate(size); |
| } |
| match format { |
| Format::Pcap => out.write(to_pcap_fmt(packet).as_slice())?, |
| Format::Pretty => out.write(to_pretty_fmt(packet).as_bytes())?, |
| }; |
| out.flush()?; |
| if let Some(count) = count { |
| pkt_count += 1; |
| if pkt_count == count { |
| break; |
| } |
| } |
| } |
| Ok(()) |
| }; |
| |
| fasync::Executor::new().context("error creating event loop")?.run_singlethreaded(main_future) |
| } |
| |
| fn main() { |
| if let Err(e) = main_res() { |
| eprintln!("Error: {}", e); |
| } |
| } |