From 59c0a9d9440a9f8219ba04899e00933758f58bb8 Mon Sep 17 00:00:00 2001 From: Will Greenberg Date: Sat, 23 Dec 2023 17:23:42 -0800 Subject: [PATCH 01/16] Use QMDL instead of our custom debug file format This might be a bit premature, but since we don't seem to be experiencing any more parsing errors when reading MessagesContainers, let's switch to outputting a standard QMDL (Qualcomm Mobile Diagnostic Log) file. This file format is more widely used by tools like scat, and the only data we lose out on is the container metadata and non-UserData messages (which we weren't using anyway). --- src/bin/wavehunter.rs | 5 +-- src/bin/wavehunter_reader.rs | 12 +++--- src/debug_file.rs | 44 --------------------- src/diag.rs | 6 +++ src/diag_device.rs | 39 ++++++------------- src/diag_reader.rs | 3 +- src/hdlc.rs | 24 ++++++------ src/lib.rs | 2 +- src/qmdl.rs | 74 ++++++++++++++++++++++++++++++++++++ 9 files changed, 115 insertions(+), 94 deletions(-) delete mode 100644 src/debug_file.rs create mode 100644 src/qmdl.rs diff --git a/src/bin/wavehunter.rs b/src/bin/wavehunter.rs index ec4628c..aa81e1f 100644 --- a/src/bin/wavehunter.rs +++ b/src/bin/wavehunter.rs @@ -8,15 +8,14 @@ use log::debug; fn main() -> DiagResult<()> { env_logger::init(); - let mut dev = DiagDevice::new()?; - dev.enable_debug_mode("/data/wavehunter/wavehunter-debug")?; + let mut dev = DiagDevice::new("./wavehunter.qmdl")?; dev.config_logs()?; println!("The orca is hunting for stingrays..."); let mut gsmtap_parser = GsmtapParser::new(); // We are going to want to add a timestamp to this pcap file eventually - let mut pcap_file = PcapFile::new("/data/wavehunter/wavehunter.pcap").unwrap(); + let mut pcap_file = PcapFile::new("./wavehunter.pcap").unwrap(); pcap_file.write_iface_header().unwrap(); loop { diff --git a/src/bin/wavehunter_reader.rs b/src/bin/wavehunter_reader.rs index db9792c..e28a2a0 100644 --- a/src/bin/wavehunter_reader.rs +++ b/src/bin/wavehunter_reader.rs @@ -1,4 +1,4 @@ -use wavehunter::debug_file::DebugFileReader; +use wavehunter::qmdl::QmdlFileReader; use wavehunter::diag_reader::DiagReader; use wavehunter::diag_device::{DiagResult, DiagDeviceError}; use wavehunter::gsmtap_parser::GsmtapParser; @@ -11,17 +11,17 @@ fn main() -> DiagResult<()> { let args: Vec = std::env::args().collect(); if args.len() != 2 { - error!("Usage: {} /path/to/debug/file", args[0]); + error!("Usage: {} /path/to/qmdl/file", args[0]); std::process::exit(1); } - let mut debug_reader = DebugFileReader::new(&args[1])?; + let mut qmdl_reader = QmdlFileReader::new(&args[1])?; let mut gsmtap_parser = GsmtapParser::new(); let mut pcap_file = PcapFile::new("./wavehunter.pcap").unwrap(); pcap_file.write_iface_header().unwrap(); loop { - match debug_reader.read_response() { + match qmdl_reader.read_response() { Ok(msgs) => { for msg in msgs { debug!("msg: {:?}", msg); @@ -32,10 +32,10 @@ fn main() -> DiagResult<()> { } }, Err(DiagDeviceError::IO(err)) if err.kind() == std::io::ErrorKind::UnexpectedEof => { - println!("Reached end of debug file, exiting..."); + println!("Reached end of QMDL file, exiting..."); std::process::exit(0); }, - Err(err) => panic!("Error reading debug file {}", err), + Err(err) => panic!("Error reading QMDL file {}", err), } } } diff --git a/src/debug_file.rs b/src/debug_file.rs deleted file mode 100644 index 55ad776..0000000 --- a/src/debug_file.rs +++ /dev/null @@ -1,44 +0,0 @@ -use crate::diag_reader::DiagReader; -use crate::diag_device::DiagResult; -use crate::diag::*; - -use deku::prelude::*; -use std::fs::File; -use std::io::Read; -use log::warn; - -#[derive(Debug, DekuRead, DekuWrite)] -#[deku(endian = "little")] -pub struct DebugFileBlock<'a> { - pub size: u32, - #[deku(count = "size")] - pub data: &'a [u8], -} - -pub struct DebugFileReader { - file: File, -} - -impl DebugFileReader { - pub fn new

(path: P) -> DiagResult where P: AsRef { - let file = std::fs::File::options() - .read(true) - .open(path)?; - Ok(DebugFileReader { file }) - } -} - -impl DiagReader for DebugFileReader { - fn get_next_messages_container(&mut self) -> DiagResult { - let mut bytes_read_buf = [0; 4]; - self.file.read_exact(&mut bytes_read_buf)?; - let bytes_read = u32::from_le_bytes(bytes_read_buf) as usize; - let mut data = vec![0; bytes_read as usize]; - self.file.read_exact(&mut data)?; - let ((leftover_bytes, _), container) = MessagesContainer::from_bytes((&data, 0))?; - if leftover_bytes.len() > 0 { - warn!("warning: {} leftover bytes when parsing MessagesContainer", leftover_bytes.len()); - } - Ok(container) - } -} diff --git a/src/diag.rs b/src/diag.rs index 3f5b2c5..2582094 100644 --- a/src/diag.rs +++ b/src/diag.rs @@ -3,6 +3,12 @@ use chrono::{DateTime, FixedOffset}; use deku::prelude::*; +pub const MESSAGE_TERMINATOR: u8 = 0x7e; +pub const MESSAGE_ESCAPE_CHAR: u8 = 0x7d; + +pub const ESCAPED_MESSAGE_TERMINATOR: u8 = 0x5e; +pub const ESCAPED_MESSAGE_ESCAPE_CHAR: u8 = 0x5d; + #[derive(Debug, Clone, DekuWrite)] pub struct RequestContainer { pub data_type: DataType, diff --git a/src/diag_device.rs b/src/diag_device.rs index 8cd9da3..70fa0af 100644 --- a/src/diag_device.rs +++ b/src/diag_device.rs @@ -1,11 +1,11 @@ use crate::hdlc::{hdlc_encapsulate, HdlcError}; use crate::diag::{Message, ResponsePayload, Request, LogConfigRequest, LogConfigResponse, build_log_mask_request, RequestContainer, DataType, MessagesContainer}; use crate::diag_reader::{DiagReader, CRC_CCITT}; -use crate::debug_file::DebugFileBlock; +use crate::qmdl::QmdlFileWriter; use crate::log_codes; use std::fs::File; -use std::io::{Read, Write}; +use std::io::Read; use std::os::fd::AsRawFd; use thiserror::Error; use log::{info, warn, error}; @@ -59,7 +59,7 @@ const DIAG_IOCTL_SWITCH_LOGGING: u32 = 7; pub struct DiagDevice { file: File, - debug_file: Option, + pub qmdl_file: QmdlFileWriter, read_buf: Vec, use_mdm: i32, } @@ -70,53 +70,36 @@ impl DiagReader for DiagDevice { while bytes_read == 0 { bytes_read = self.file.read(&mut self.read_buf)?; } - if let Some(debug_file) = self.debug_file.as_mut() { - let debug_block = DebugFileBlock { - size: bytes_read as u32, - data: &self.read_buf[0..bytes_read], - }; - let debug_block_bytes = debug_block.to_bytes()?; - debug_file.write_all(&debug_block_bytes)?; - } let ((leftover_bytes, _), container) = MessagesContainer::from_bytes((&self.read_buf[0..bytes_read], 0))?; if leftover_bytes.len() > 0 { warn!("warning: {} leftover bytes when parsing MessagesContainer", leftover_bytes.len()); } + self.qmdl_file.write_container(&container)?; Ok(container) } } impl DiagDevice { - pub fn new() -> DiagResult { - let file = std::fs::File::options() + pub fn new

(qmdl_path: P) -> DiagResult where P: AsRef { + let diag_file = std::fs::File::options() .read(true) .write(true) .open("/dev/diag")?; - let fd = file.as_raw_fd(); + let fd = diag_file.as_raw_fd(); + + let qmdl_file = QmdlFileWriter::new(qmdl_path)?; enable_frame_readwrite(fd, MEMORY_DEVICE_MODE)?; let use_mdm = determine_use_mdm(fd)?; Ok(DiagDevice { read_buf: vec![0; BUFFER_LEN], - file, - debug_file: None, + file: diag_file, + qmdl_file, use_mdm, }) } - // Creates a file at the given path where all binary output from /dev/diag - // will be recorded. - pub fn enable_debug_mode

(&mut self, path: P) -> DiagResult<()> where P: AsRef { - let debug_file = std::fs::File::options() - .create(true) - .write(true) - .open(path)?; - info!("enabling debug mode, writing debug output to {:?}", debug_file); - self.debug_file = Some(debug_file); - Ok(()) - } - pub fn write_request(&mut self, req: &Request) -> DiagResult<()> { let buf = RequestContainer { data_type: DataType::UserSpace, diff --git a/src/diag_reader.rs b/src/diag_reader.rs index b8d2d85..3a1cfaa 100644 --- a/src/diag_reader.rs +++ b/src/diag_reader.rs @@ -1,3 +1,4 @@ +use crate::diag; use crate::{diag::*, hdlc::hdlc_decapsulate}; use crate::diag_device::DiagResult; @@ -36,7 +37,7 @@ pub trait DiagReader { fn parse_response_container(&self, container: MessagesContainer) -> DiagResult> { let mut result = Vec::new(); for msg in container.messages { - for sub_msg in msg.data.split_inclusive(|&b| b == 0x7e) { + for sub_msg in msg.data.split_inclusive(|&b| b == diag::MESSAGE_TERMINATOR) { match hdlc_decapsulate(&sub_msg, &CRC_CCITT) { Ok(data) => match Message::from_bytes((&data, 0)) { Ok(((leftover_bytes, _), res)) => { diff --git a/src/hdlc.rs b/src/hdlc.rs index 7813e5b..9e0ba0f 100644 --- a/src/hdlc.rs +++ b/src/hdlc.rs @@ -7,6 +7,8 @@ use crc::Crc; use bytes::Buf; use thiserror::Error; +use crate::diag::{MESSAGE_ESCAPE_CHAR, MESSAGE_TERMINATOR, ESCAPED_MESSAGE_ESCAPE_CHAR, ESCAPED_MESSAGE_TERMINATOR}; + #[derive(Debug, Error, PartialEq)] pub enum HdlcError { #[error("Invalid checksum (expected {0}, got {1})")] @@ -22,25 +24,25 @@ pub enum HdlcError { } pub fn hdlc_encapsulate(data: &[u8], crc: &Crc) -> Vec { - let mut result: Vec = vec![]; + let mut result: Vec = Vec::with_capacity(data.len()); for &b in data { match b { - 0x7e => result.extend([0x7d, 0x5e]), - 0x7d => result.extend([0x7d, 0x5d]), + MESSAGE_TERMINATOR => result.extend([MESSAGE_ESCAPE_CHAR, ESCAPED_MESSAGE_TERMINATOR]), + MESSAGE_ESCAPE_CHAR => result.extend([MESSAGE_ESCAPE_CHAR, ESCAPED_MESSAGE_ESCAPE_CHAR]), _ => result.push(b), } } for b in crc.checksum(&data).to_le_bytes() { match b { - 0x7e => result.extend([0x7d, 0x5e]), - 0x7d => result.extend([0x7d, 0x5d]), + MESSAGE_TERMINATOR => result.extend([MESSAGE_ESCAPE_CHAR, ESCAPED_MESSAGE_TERMINATOR]), + MESSAGE_ESCAPE_CHAR => result.extend([MESSAGE_ESCAPE_CHAR, ESCAPED_MESSAGE_ESCAPE_CHAR]), _ => result.push(b), } } - result.push(0x7e); + result.push(MESSAGE_TERMINATOR); result } @@ -49,21 +51,21 @@ pub fn hdlc_decapsulate(data: &[u8], crc: &Crc) -> Result, HdlcErro return Err(HdlcError::TooShort); } - if data[data.len() - 1] != 0x7e { + if data[data.len() - 1] != MESSAGE_TERMINATOR { return Err(HdlcError::NoTrailingCharacter(data[data.len() - 1])); } - let mut unescaped = Vec::new(); + let mut unescaped = Vec::with_capacity(data.len()); let mut escaping = false; for &b in &data[..data.len() - 1] { if escaping { match b { - 0x5e => unescaped.push(0x7e), - 0x5d => unescaped.push(0x7d), + ESCAPED_MESSAGE_TERMINATOR => unescaped.push(MESSAGE_TERMINATOR), + ESCAPED_MESSAGE_ESCAPE_CHAR => unescaped.push(MESSAGE_ESCAPE_CHAR), _ => return Err(HdlcError::InvalidEscapeSequence(b)), } escaping = false; - } else if b == 0x7d { + } else if b == MESSAGE_ESCAPE_CHAR { escaping = true } else { unescaped.push(b); diff --git a/src/lib.rs b/src/lib.rs index 3a9f757..61de5e9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ pub mod hdlc; pub mod diag; pub mod diag_device; pub mod diag_reader; -pub mod debug_file; +pub mod qmdl; pub mod log_codes; pub mod gsmtap; pub mod gsmtap_parser; diff --git a/src/qmdl.rs b/src/qmdl.rs new file mode 100644 index 0000000..fdc17aa --- /dev/null +++ b/src/qmdl.rs @@ -0,0 +1,74 @@ +//! QMDL files are Qualcomm Mobile Diagnostic Logs. Their format is very simple, +//! just a series of of concatenated HDLC encapsulated diag::Message structs. + +use crate::diag_reader::DiagReader; +use crate::diag_device::DiagResult; +use crate::diag::{MessagesContainer, MESSAGE_TERMINATOR, HdlcEncapsulatedMessage, DataType}; + +use std::fs::File; +use std::io::{Write, BufReader, BufRead}; + +pub struct QmdlFileWriter { + file: File, + pub total_written: usize, +} + +impl QmdlFileWriter { + pub fn new

(path: P) -> DiagResult where P: AsRef { + let file = std::fs::File::options() + .create(true) + .append(true) + .open(path)?; + Ok(QmdlFileWriter { + file, + total_written: 0, + }) + } + + pub fn write_container(&mut self, container: &MessagesContainer) -> DiagResult<()> { + for msg in &container.messages { + self.file.write_all(&msg.data)?; + self.total_written += msg.data.len(); + } + Ok(()) + } +} + +pub struct QmdlFileReader { + file: BufReader, + buf: Vec +} + +impl QmdlFileReader { + pub fn new

(path: P) -> DiagResult where P: AsRef { + let file = std::fs::File::options() + .read(true) + .open(path)?; + Ok(QmdlFileReader { + file: BufReader::new(file), + buf: Vec::new(), + }) + } +} + +impl DiagReader for QmdlFileReader { + fn get_next_messages_container(&mut self) -> DiagResult { + let bytes_read = self.file.read_until(MESSAGE_TERMINATOR, &mut self.buf)?; + + // Since QMDL is just a flat list of messages, we can't actually + // reproduce the container structure they came from in the original + // read. So we'll just pretend that all containers had exactly one + // message. As far as I know, the number of messages per container + // doesn't actually affect anything, so this should be fine. + Ok(MessagesContainer { + data_type: DataType::UserSpace, + num_messages: 1, + messages: vec![ + HdlcEncapsulatedMessage { + len: 1, + data: self.buf[0..bytes_read].to_vec(), + }, + ] + }) + } +} From 0f2e4676e54bdb5991c6dbd9a97a56b7d36b80fb Mon Sep 17 00:00:00 2001 From: Will Greenberg Date: Sun, 24 Dec 2023 12:52:23 -0800 Subject: [PATCH 02/16] Refactor error types and add config file parsing 1. Main binary now parses a config toml and uses its paths for the output pcap and qmdl files 2. Previously we were relying on DiagDeviceError for several things that aren't a diag device. This modularizes that error in a way that both improves the error descriptions, and also allows for better separation of concerns. --- Cargo.lock | 61 +++++++++++++++++++++++++++- Cargo.toml | 3 ++ src/bin/wavehunter.rs | 79 +++++++++++++++++++++++++++++++----- src/bin/wavehunter_reader.rs | 7 ++-- src/diag_device.rs | 44 ++++++++++++-------- src/diag_reader.rs | 9 ++-- src/qmdl.rs | 11 ++--- 7 files changed, 173 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b720e2e..7e46315 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -372,7 +372,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "toml_edit", + "toml_edit 0.19.15", ] [[package]] @@ -441,6 +441,35 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "serde" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + [[package]] name = "strsim" version = "0.10.0" @@ -504,11 +533,26 @@ dependencies = [ "syn 2.0.39", ] +[[package]] +name = "toml" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.21.0", +] + [[package]] name = "toml_datetime" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -521,6 +565,19 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "unicode-ident" version = "1.0.12" @@ -593,7 +650,9 @@ dependencies = [ "libc", "log", "pcap-file", + "serde", "thiserror", + "toml", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 10c1465..54aeec0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,4 +27,7 @@ libc = "0.2.150" log = "0.4.20" pcap-file = "2.0.0" thiserror = "1.0.50" +# TODO: split into binary-only dependencies +toml = "0.8.8" +serde = { version = "1.0.193", features = ["derive"] } diff --git a/src/bin/wavehunter.rs b/src/bin/wavehunter.rs index aa81e1f..68708a6 100644 --- a/src/bin/wavehunter.rs +++ b/src/bin/wavehunter.rs @@ -1,29 +1,86 @@ -use wavehunter::diag_device::{DiagDevice, DiagResult}; +use wavehunter::diag_device::{DiagDevice, DiagDeviceError}; use wavehunter::diag_reader::DiagReader; -use wavehunter::gsmtap_parser::GsmtapParser; -use wavehunter::pcap::PcapFile; +use wavehunter::gsmtap_parser::{GsmtapParser, GsmtapParserError}; +use wavehunter::pcap::{PcapFile, PcapFileError}; use log::debug; +use thiserror::Error; +use serde::Deserialize; +use toml; -fn main() -> DiagResult<()> { +#[derive(Error, Debug)] +enum WavehunterError { + #[error("Missing config file: {0}")] + MissingConfigFile(String), + #[error("Config file parsing error: {0}")] + ConfigFileParsingError(#[from] toml::de::Error), + #[error("Pcap file initialization error: {0}")] + PcapFileInitError(PcapFileError), + #[error("Pcap file write error: {0}")] + PcapFileWriteError(PcapFileError), + #[error("Diag intialization error: {0}")] + DiagInitError(DiagDeviceError), + #[error("Diag read error: {0}")] + DiagReadError(DiagDeviceError), + #[error("GSMTAP parsing error: {0}")] + GsmtapParsingError(GsmtapParserError), +} + +#[derive(Deserialize)] +struct ConfigFile { + qmdl_path: Option, + pcap_path: Option, +} + +#[derive(Debug)] +struct Config { + qmdl_path: String, + pcap_path: String, +} + +fn parse_config

(path: P) -> Result where P: AsRef { + let config_file = std::fs::read_to_string(&path) + .map_err(|_| WavehunterError::MissingConfigFile(format!("{:?}", path.as_ref())))?; + let parsed_config: ConfigFile = toml::from_str(&config_file) + .map_err(WavehunterError::ConfigFileParsingError)?; + Ok(Config { + qmdl_path: parsed_config.qmdl_path.unwrap_or("./wavehunter.qmdl".to_string()), + pcap_path: parsed_config.pcap_path.unwrap_or("./wavehunter.pcap".to_string()), + }) +} + +fn main() -> Result<(), WavehunterError> { env_logger::init(); - let mut dev = DiagDevice::new("./wavehunter.qmdl")?; - dev.config_logs()?; + let args: Vec = std::env::args().collect(); + if args.len() != 2 { + println!("Usage: {} /path/to/config/file", args[0]); + std::process::exit(1); + } + + let config = parse_config(&args[1])?; + + let mut dev = DiagDevice::new(&config.qmdl_path) + .map_err(WavehunterError::DiagInitError)?; + dev.config_logs() + .map_err(WavehunterError::DiagInitError)?; println!("The orca is hunting for stingrays..."); let mut gsmtap_parser = GsmtapParser::new(); // We are going to want to add a timestamp to this pcap file eventually - let mut pcap_file = PcapFile::new("./wavehunter.pcap").unwrap(); - pcap_file.write_iface_header().unwrap(); + let mut pcap_file = PcapFile::new(&config.pcap_path) + .map_err(WavehunterError::PcapFileInitError)?; + pcap_file.write_iface_header() + .map_err(WavehunterError::PcapFileWriteError)?; loop { - for msg in dev.read_response()? { + for msg in dev.read_response().map_err(WavehunterError::DiagReadError)? { debug!("msg: {:?}", msg); - if let Some((timestamp, gsmtap_msg)) = gsmtap_parser.recv_message(msg).unwrap() { + if let Some((timestamp, gsmtap_msg)) = gsmtap_parser.recv_message(msg).map_err(WavehunterError::GsmtapParsingError)? { debug!("gsmtap_msg: {:?}", gsmtap_msg); - pcap_file.write_gsmtap_message(gsmtap_msg, timestamp).unwrap(); + pcap_file.write_gsmtap_message(gsmtap_msg, timestamp) + .map_err(WavehunterError::PcapFileWriteError)?; } } } diff --git a/src/bin/wavehunter_reader.rs b/src/bin/wavehunter_reader.rs index e28a2a0..6293c3d 100644 --- a/src/bin/wavehunter_reader.rs +++ b/src/bin/wavehunter_reader.rs @@ -1,12 +1,11 @@ use wavehunter::qmdl::QmdlFileReader; use wavehunter::diag_reader::DiagReader; -use wavehunter::diag_device::{DiagResult, DiagDeviceError}; use wavehunter::gsmtap_parser::GsmtapParser; use wavehunter::pcap::PcapFile; use log::{debug, error}; -fn main() -> DiagResult<()> { +fn main() { env_logger::init(); let args: Vec = std::env::args().collect(); @@ -14,7 +13,7 @@ fn main() -> DiagResult<()> { error!("Usage: {} /path/to/qmdl/file", args[0]); std::process::exit(1); } - let mut qmdl_reader = QmdlFileReader::new(&args[1])?; + let mut qmdl_reader = QmdlFileReader::new(&args[1]).unwrap(); let mut gsmtap_parser = GsmtapParser::new(); let mut pcap_file = PcapFile::new("./wavehunter.pcap").unwrap(); @@ -31,7 +30,7 @@ fn main() -> DiagResult<()> { } } }, - Err(DiagDeviceError::IO(err)) if err.kind() == std::io::ErrorKind::UnexpectedEof => { + Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => { println!("Reached end of QMDL file, exiting..."); std::process::exit(0); }, diff --git a/src/diag_device.rs b/src/diag_device.rs index 70fa0af..c6db4e4 100644 --- a/src/diag_device.rs +++ b/src/diag_device.rs @@ -1,4 +1,4 @@ -use crate::hdlc::{hdlc_encapsulate, HdlcError}; +use crate::hdlc::hdlc_encapsulate; use crate::diag::{Message, ResponsePayload, Request, LogConfigRequest, LogConfigResponse, build_log_mask_request, RequestContainer, DataType, MessagesContainer}; use crate::diag_reader::{DiagReader, CRC_CCITT}; use crate::qmdl::QmdlFileWriter; @@ -15,20 +15,24 @@ pub type DiagResult = Result; #[derive(Error, Debug)] pub enum DiagDeviceError { - #[error("IO error {0}")] - IO(#[from] std::io::Error), #[error("Failed to initialize /dev/diag: {0}")] InitializationFailed(String), #[error("Failed to read diag device: {0}")] - DeviceReadFailed(String), + DeviceReadFailed(std::io::Error), + #[error("Failed to write diag device: {0}")] + DeviceWriteFailed(String), #[error("Nonzero status code {0} for diag request: {1:?}")] RequestFailed(u32, Request), #[error("Didn't receive response for request: {0:?}")] NoResponse(Request), - #[error("HDLC error {0}")] - HdlcError(#[from] HdlcError), - #[error("Deku error {0}")] - DekuError(#[from] DekuError), + #[error("Failed to open QMDL file: {0}")] + OpenQmdlFileError(std::io::Error), + #[error("Failed to write to QMDL file: {0}")] + QmdlFileWriteError(std::io::Error), + #[error("Failed to open diag device: {0}")] + OpenDiagDeviceError(std::io::Error), + #[error("Failed to parse MessagesContainer: {0}")] + ParseMessagesContainerError(deku::DekuError), } pub const LOG_CODES_FOR_RAW_PACKET_LOGGING: [u32; 11] = [ @@ -65,16 +69,21 @@ pub struct DiagDevice { } impl DiagReader for DiagDevice { + type Err = DiagDeviceError; + fn get_next_messages_container(&mut self) -> DiagResult { let mut bytes_read = 0; while bytes_read == 0 { - bytes_read = self.file.read(&mut self.read_buf)?; + bytes_read = self.file.read(&mut self.read_buf) + .map_err(DiagDeviceError::DeviceReadFailed)?; } - let ((leftover_bytes, _), container) = MessagesContainer::from_bytes((&self.read_buf[0..bytes_read], 0))?; + let ((leftover_bytes, _), container) = MessagesContainer::from_bytes((&self.read_buf[0..bytes_read], 0)) + .map_err(DiagDeviceError::ParseMessagesContainerError)?; if leftover_bytes.len() > 0 { warn!("warning: {} leftover bytes when parsing MessagesContainer", leftover_bytes.len()); } - self.qmdl_file.write_container(&container)?; + self.qmdl_file.write_container(&container) + .map_err(DiagDeviceError::QmdlFileWriteError)?; Ok(container) } } @@ -84,10 +93,12 @@ impl DiagDevice { let diag_file = std::fs::File::options() .read(true) .write(true) - .open("/dev/diag")?; + .open("/dev/diag") + .map_err(DiagDeviceError::OpenDiagDeviceError)?; let fd = diag_file.as_raw_fd(); - let qmdl_file = QmdlFileWriter::new(qmdl_path)?; + let qmdl_file = QmdlFileWriter::new(qmdl_path) + .map_err(DiagDeviceError::OpenQmdlFileError)?; enable_frame_readwrite(fd, MEMORY_DEVICE_MODE)?; let use_mdm = determine_use_mdm(fd)?; @@ -101,19 +112,20 @@ impl DiagDevice { } pub fn write_request(&mut self, req: &Request) -> DiagResult<()> { + let req_bytes = &req.to_bytes().expect("Failed to serialize Request"); let buf = RequestContainer { data_type: DataType::UserSpace, use_mdm: self.use_mdm > 0, mdm_field: -1, - hdlc_encapsulated_request: hdlc_encapsulate(&req.to_bytes()?, &CRC_CCITT), - }.to_bytes()?; + hdlc_encapsulated_request: hdlc_encapsulate(&req_bytes, &CRC_CCITT), + }.to_bytes().expect("Failed to serialize RequestContainer"); unsafe { let fd = self.file.as_raw_fd(); let buf_ptr = buf.as_ptr() as *const libc::c_void; let ret = libc::write(fd, buf_ptr, buf.len()); if ret < 0 { let msg = format!("write failed with error code {}", ret); - return Err(DiagDeviceError::DeviceReadFailed(msg)); + return Err(DiagDeviceError::DeviceWriteFailed(msg)); } } Ok(()) diff --git a/src/diag_reader.rs b/src/diag_reader.rs index 3a1cfaa..0b6ed3e 100644 --- a/src/diag_reader.rs +++ b/src/diag_reader.rs @@ -1,6 +1,5 @@ use crate::diag; use crate::{diag::*, hdlc::hdlc_decapsulate}; -use crate::diag_device::DiagResult; use crc::{Crc, Algorithm}; use deku::prelude::*; @@ -21,9 +20,11 @@ pub const CRC_CCITT_ALG: Algorithm = Algorithm { pub const CRC_CCITT: Crc = Crc::::new(&CRC_CCITT_ALG); pub trait DiagReader { - fn get_next_messages_container(&mut self) -> DiagResult; + type Err; - fn read_response(&mut self) -> DiagResult> { + fn get_next_messages_container(&mut self) -> Result; + + fn read_response(&mut self) -> Result, Self::Err> { loop { let container = self.get_next_messages_container()?; if container.data_type == DataType::UserSpace { @@ -34,7 +35,7 @@ pub trait DiagReader { } } - fn parse_response_container(&self, container: MessagesContainer) -> DiagResult> { + fn parse_response_container(&self, container: MessagesContainer) -> Result, Self::Err> { let mut result = Vec::new(); for msg in container.messages { for sub_msg in msg.data.split_inclusive(|&b| b == diag::MESSAGE_TERMINATOR) { diff --git a/src/qmdl.rs b/src/qmdl.rs index fdc17aa..d2e9f6e 100644 --- a/src/qmdl.rs +++ b/src/qmdl.rs @@ -2,7 +2,6 @@ //! just a series of of concatenated HDLC encapsulated diag::Message structs. use crate::diag_reader::DiagReader; -use crate::diag_device::DiagResult; use crate::diag::{MessagesContainer, MESSAGE_TERMINATOR, HdlcEncapsulatedMessage, DataType}; use std::fs::File; @@ -14,7 +13,7 @@ pub struct QmdlFileWriter { } impl QmdlFileWriter { - pub fn new

(path: P) -> DiagResult where P: AsRef { + pub fn new

(path: P) -> std::io::Result where P: AsRef { let file = std::fs::File::options() .create(true) .append(true) @@ -25,7 +24,7 @@ impl QmdlFileWriter { }) } - pub fn write_container(&mut self, container: &MessagesContainer) -> DiagResult<()> { + pub fn write_container(&mut self, container: &MessagesContainer) -> std::io::Result<()> { for msg in &container.messages { self.file.write_all(&msg.data)?; self.total_written += msg.data.len(); @@ -40,7 +39,7 @@ pub struct QmdlFileReader { } impl QmdlFileReader { - pub fn new

(path: P) -> DiagResult where P: AsRef { + pub fn new

(path: P) -> std::io::Result where P: AsRef { let file = std::fs::File::options() .read(true) .open(path)?; @@ -52,7 +51,9 @@ impl QmdlFileReader { } impl DiagReader for QmdlFileReader { - fn get_next_messages_container(&mut self) -> DiagResult { + type Err = std::io::Error; + + fn get_next_messages_container(&mut self) -> std::io::Result { let bytes_read = self.file.read_until(MESSAGE_TERMINATOR, &mut self.buf)?; // Since QMDL is just a flat list of messages, we can't actually From ef42dfe99f5596774e37be819bd0c4ef5356e8e6 Mon Sep 17 00:00:00 2001 From: Will Greenberg Date: Sun, 24 Dec 2023 13:24:37 -0800 Subject: [PATCH 03/16] diag_reader: raise parsing/hdlc error handling to caller This'll let us unit test the reader more easily --- src/bin/wavehunter.rs | 21 +++++++++++++++------ src/bin/wavehunter_reader.rs | 24 ++++++++++++------------ src/diag_device.rs | 10 ++++++---- src/diag_reader.rs | 26 ++++++++++++++++++-------- 4 files changed, 51 insertions(+), 30 deletions(-) diff --git a/src/bin/wavehunter.rs b/src/bin/wavehunter.rs index 68708a6..5a014c2 100644 --- a/src/bin/wavehunter.rs +++ b/src/bin/wavehunter.rs @@ -75,12 +75,21 @@ fn main() -> Result<(), WavehunterError> { .map_err(WavehunterError::PcapFileWriteError)?; loop { - for msg in dev.read_response().map_err(WavehunterError::DiagReadError)? { - debug!("msg: {:?}", msg); - if let Some((timestamp, gsmtap_msg)) = gsmtap_parser.recv_message(msg).map_err(WavehunterError::GsmtapParsingError)? { - debug!("gsmtap_msg: {:?}", gsmtap_msg); - pcap_file.write_gsmtap_message(gsmtap_msg, timestamp) - .map_err(WavehunterError::PcapFileWriteError)?; + for maybe_msg in dev.read_response().map_err(WavehunterError::DiagReadError)? { + match maybe_msg { + Ok(msg) => { + debug!("msg: {:?}", msg); + let maybe_gsmtap_msg = gsmtap_parser.recv_message(msg) + .map_err(WavehunterError::GsmtapParsingError)?; + if let Some((timestamp, gsmtap_msg)) = maybe_gsmtap_msg { + debug!("gsmtap_msg: {:?}", gsmtap_msg); + pcap_file.write_gsmtap_message(gsmtap_msg, timestamp) + .map_err(WavehunterError::PcapFileWriteError)?; + } + }, + Err(e) => { + dbg!("error parsing message: {:?}", e); + }, } } } diff --git a/src/bin/wavehunter_reader.rs b/src/bin/wavehunter_reader.rs index 6293c3d..470d054 100644 --- a/src/bin/wavehunter_reader.rs +++ b/src/bin/wavehunter_reader.rs @@ -20,21 +20,21 @@ fn main() { pcap_file.write_iface_header().unwrap(); loop { - match qmdl_reader.read_response() { - Ok(msgs) => { - for msg in msgs { + for maybe_msg in qmdl_reader.read_response().expect("error reading qmdl file") { + match maybe_msg { + Ok(msg) => { debug!("msg: {:?}", msg); - if let Some((timestamp, gsmtap_msg)) = gsmtap_parser.recv_message(msg).unwrap() { + let maybe_gsmtap_msg = gsmtap_parser.recv_message(msg).expect("error parsing gsmtap message"); + if let Some((timestamp, gsmtap_msg)) = maybe_gsmtap_msg { debug!("gsmtap_msg: {:?}", gsmtap_msg); - pcap_file.write_gsmtap_message(gsmtap_msg, timestamp).unwrap(); + pcap_file.write_gsmtap_message(gsmtap_msg, timestamp) + .expect("error writing pcap packet"); } - } - }, - Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => { - println!("Reached end of QMDL file, exiting..."); - std::process::exit(0); - }, - Err(err) => panic!("Error reading QMDL file {}", err), + }, + Err(e) => { + dbg!("error parsing message: {:?}", e); + }, + } } } } diff --git a/src/diag_device.rs b/src/diag_device.rs index c6db4e4..8cd2fea 100644 --- a/src/diag_device.rs +++ b/src/diag_device.rs @@ -137,8 +137,8 @@ impl DiagDevice { for msg in self.read_response()? { match msg { - Message::Log { .. } => info!("skipping log response..."), - Message::Response { payload, status, .. } => match payload { + Ok(Message::Log { .. }) => info!("skipping log response..."), + Ok(Message::Response { payload, status, .. }) => match payload { ResponsePayload::LogConfig(LogConfigResponse::RetrieveIdRanges { log_mask_sizes }) => { if status != 0 { return Err(DiagDeviceError::RequestFailed(status, req)); @@ -147,6 +147,7 @@ impl DiagDevice { }, _ => info!("skipping non-LogConfigResponse response..."), }, + Err(e) => error!("error parsing message: {:?}", e), } } @@ -159,8 +160,8 @@ impl DiagDevice { for msg in self.read_response()? { match msg { - Message::Log { .. } => info!("skipping log response..."), - Message::Response { payload, status, .. } => { + Ok(Message::Log { .. }) => info!("skipping log response..."), + Ok(Message::Response { payload, status, .. }) => { if let ResponsePayload::LogConfig(LogConfigResponse::SetMask) = payload { if status != 0 { return Err(DiagDeviceError::RequestFailed(status, req)); @@ -168,6 +169,7 @@ impl DiagDevice { return Ok(()); } }, + Err(e) => error!("error parsing message: {:?}", e), } } diff --git a/src/diag_reader.rs b/src/diag_reader.rs index 0b6ed3e..042bf37 100644 --- a/src/diag_reader.rs +++ b/src/diag_reader.rs @@ -1,9 +1,11 @@ use crate::diag; use crate::{diag::*, hdlc::hdlc_decapsulate}; +use crate::hdlc; use crc::{Crc, Algorithm}; use deku::prelude::*; -use log::{debug, info, warn, error}; +use log::{info, warn, error}; +use thiserror::Error; // this is sorta based on the params qcsuper uses, plus what seems to be used in // https://github.com/fgsect/scat/blob/f1538b397721df3ab8ba12acd26716abcf21f78b/util.py#L47 @@ -19,12 +21,22 @@ pub const CRC_CCITT_ALG: Algorithm = Algorithm { }; pub const CRC_CCITT: Crc = Crc::::new(&CRC_CCITT_ALG); +#[derive(Debug, Error)] +pub enum DiagParsingError { + #[error("Failed to parse Message: {0}, data: {1:?}")] + MessageParsingError(deku::DekuError, Vec), + #[error("HDLC decapsulation of message failed: {0}, data: {1:?}")] + HdlcDecapsulationError(hdlc::HdlcError, Vec), +} + +type MaybeMessage = Result; + pub trait DiagReader { type Err; fn get_next_messages_container(&mut self) -> Result; - fn read_response(&mut self) -> Result, Self::Err> { + fn read_response(&mut self) -> Result, Self::Err> { loop { let container = self.get_next_messages_container()?; if container.data_type == DataType::UserSpace { @@ -35,7 +47,7 @@ pub trait DiagReader { } } - fn parse_response_container(&self, container: MessagesContainer) -> Result, Self::Err> { + fn parse_response_container(&self, container: MessagesContainer) -> Result, Self::Err> { let mut result = Vec::new(); for msg in container.messages { for sub_msg in msg.data.split_inclusive(|&b| b == diag::MESSAGE_TERMINATOR) { @@ -45,16 +57,14 @@ pub trait DiagReader { if leftover_bytes.len() > 0 { warn!("warning: {} leftover bytes when parsing Message", leftover_bytes.len()); } - result.push(res); + result.push(Ok(res)); }, Err(e) => { - error!("error parsing response: {:?}", e); - debug!("{:?}", data); + result.push(Err(DiagParsingError::MessageParsingError(e, data))); }, }, Err(err) => { - error!("error decapsulating response: {:?}", err); - debug!("{:?}", &sub_msg); + result.push(Err(DiagParsingError::HdlcDecapsulationError(err, sub_msg.to_vec()))); } } } From 38f8a78b6085a775fb3a4d54e81fe9db1d1c373b Mon Sep 17 00:00:00 2001 From: Will Greenberg Date: Wed, 27 Dec 2023 15:35:39 -0800 Subject: [PATCH 04/16] Refactor QMDL and DiagReader for better testing For the QMDL code, this mostly rewrites the structs to accept generic types that implement Read and Write, which allows for better unit testing. Also adds a bunch of unit tests. --- src/diag.rs | 16 ++-- src/diag_device.rs | 34 ++++++-- src/diag_reader.rs | 151 ++++++++++++++++++++++++++++++++- src/qmdl.rs | 207 +++++++++++++++++++++++++++++++++++++-------- 4 files changed, 356 insertions(+), 52 deletions(-) diff --git a/src/diag.rs b/src/diag.rs index 2582094..786476d 100644 --- a/src/diag.rs +++ b/src/diag.rs @@ -49,7 +49,7 @@ pub enum DataType { Other(u32), } -#[derive(Debug, Clone, DekuRead)] +#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)] pub struct MessagesContainer { pub data_type: DataType, pub num_messages: u32, @@ -57,14 +57,14 @@ pub struct MessagesContainer { pub messages: Vec, } -#[derive(Debug, Clone, DekuRead)] +#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)] pub struct HdlcEncapsulatedMessage { pub len: u32, #[deku(count = "len")] pub data: Vec, } -#[derive(Debug, Clone, PartialEq, DekuRead)] +#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)] #[deku(type = "u8")] pub enum Message { #[deku(id = "16")] @@ -93,7 +93,7 @@ pub enum Message { }, } -#[derive(Debug, Clone, PartialEq, DekuRead)] +#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)] #[deku(ctx = "log_type: u16, hdr_len: u16", id = "log_type")] pub enum LogBody { #[deku(id = "0x412f")] @@ -161,7 +161,7 @@ pub enum LogBody { } } -#[derive(Debug, Clone, PartialEq, DekuRead)] +#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)] #[deku(ctx = "ext_header_version: u8", id = "ext_header_version")] pub enum LteRrcOtaPacket { #[deku(id_pat = "0..=4")] @@ -268,7 +268,7 @@ impl LteRrcOtaPacket { } } -#[derive(Debug, Clone, PartialEq, DekuRead)] +#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)] #[deku(endian = "little")] pub struct Timestamp { pub ts: u64, @@ -288,14 +288,14 @@ impl Timestamp { } } -#[derive(Debug, Clone, PartialEq, DekuRead)] +#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)] #[deku(ctx = "opcode: u32, subopcode: u32", id = "opcode")] pub enum ResponsePayload { #[deku(id = "115")] LogConfig(#[deku(ctx = "subopcode")] LogConfigResponse), } -#[derive(Debug, Clone, PartialEq, DekuRead)] +#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)] #[deku(ctx = "subopcode: u32", id = "subopcode")] pub enum LogConfigResponse { #[deku(id = "1")] diff --git a/src/diag_device.rs b/src/diag_device.rs index 8cd2fea..574c083 100644 --- a/src/diag_device.rs +++ b/src/diag_device.rs @@ -1,7 +1,7 @@ use crate::hdlc::hdlc_encapsulate; use crate::diag::{Message, ResponsePayload, Request, LogConfigRequest, LogConfigResponse, build_log_mask_request, RequestContainer, DataType, MessagesContainer}; use crate::diag_reader::{DiagReader, CRC_CCITT}; -use crate::qmdl::QmdlFileWriter; +use crate::qmdl::QmdlWriter; use crate::log_codes; use std::fs::File; @@ -44,14 +44,14 @@ pub const LOG_CODES_FOR_RAW_PACKET_LOGGING: [u32; 11] = [ log_codes::WCDMA_SIGNALLING_MESSAGE, // 0x412f log_codes::LOG_LTE_RRC_OTA_MSG_LOG_C, // 0xb0c0 log_codes::LOG_NR_RRC_OTA_MSG_LOG_C, // 0xb821 - + // NAS: log_codes::LOG_UMTS_NAS_OTA_MESSAGE_LOG_PACKET_C, // 0x713a log_codes::LOG_LTE_NAS_ESM_OTA_IN_MSG_LOG_C, // 0xb0e2 log_codes::LOG_LTE_NAS_ESM_OTA_OUT_MSG_LOG_C, // 0xb0e3 log_codes::LOG_LTE_NAS_EMM_OTA_IN_MSG_LOG_C, // 0xb0ec log_codes::LOG_LTE_NAS_EMM_OTA_OUT_MSG_LOG_C, // 0xb0ed - + // User IP traffic: log_codes::LOG_DATA_PROTOCOL_LOGGING_C // 0x11eb ]; @@ -63,7 +63,8 @@ const DIAG_IOCTL_SWITCH_LOGGING: u32 = 7; pub struct DiagDevice { file: File, - pub qmdl_file: QmdlFileWriter, + pub qmdl_writer: QmdlWriter, + fully_initialized: bool, read_buf: Vec, use_mdm: i32, } @@ -82,8 +83,11 @@ impl DiagReader for DiagDevice { if leftover_bytes.len() > 0 { warn!("warning: {} leftover bytes when parsing MessagesContainer", leftover_bytes.len()); } - self.qmdl_file.write_container(&container) - .map_err(DiagDeviceError::QmdlFileWriteError)?; + + if self.fully_initialized { + self.qmdl_writer.write_container(&container) + .map_err(DiagDeviceError::QmdlFileWriteError)?; + } Ok(container) } } @@ -97,8 +101,20 @@ impl DiagDevice { .map_err(DiagDeviceError::OpenDiagDeviceError)?; let fd = diag_file.as_raw_fd(); - let qmdl_file = QmdlFileWriter::new(qmdl_path) + let qmdl_file = File::options() + .create(true) + .append(true) + .open(&qmdl_path) .map_err(DiagDeviceError::OpenQmdlFileError)?; + let qmdl_metadata = qmdl_file.metadata().map_err(DiagDeviceError::OpenQmdlFileError)?; + if qmdl_metadata.len() != 0 { + info!( + "QMDL file {} already contains data ({} bytes), appending to it", + qmdl_path.as_ref().display(), + qmdl_metadata.len() + ); + } + let qmdl_writer = QmdlWriter::new_with_existing_size(qmdl_file, qmdl_metadata.len() as usize); enable_frame_readwrite(fd, MEMORY_DEVICE_MODE)?; let use_mdm = determine_use_mdm(fd)?; @@ -106,7 +122,8 @@ impl DiagDevice { Ok(DiagDevice { read_buf: vec![0; BUFFER_LEN], file: diag_file, - qmdl_file, + fully_initialized: false, + qmdl_writer, use_mdm, }) } @@ -187,6 +204,7 @@ impl DiagDevice { } } + self.fully_initialized = true; Ok(()) } } diff --git a/src/diag_reader.rs b/src/diag_reader.rs index 042bf37..c0f1678 100644 --- a/src/diag_reader.rs +++ b/src/diag_reader.rs @@ -21,7 +21,7 @@ pub const CRC_CCITT_ALG: Algorithm = Algorithm { }; pub const CRC_CCITT: Crc = Crc::::new(&CRC_CCITT_ALG); -#[derive(Debug, Error)] +#[derive(Debug, PartialEq, Error)] pub enum DiagParsingError { #[error("Failed to parse Message: {0}, data: {1:?}")] MessageParsingError(deku::DekuError, Vec), @@ -72,3 +72,152 @@ pub trait DiagReader { Ok(result) } } + +#[cfg(test)] +mod test { + use super::*; + + struct MockReader { + containers: Vec, + } + + impl DiagReader for MockReader { + type Err = (); + + fn get_next_messages_container(&mut self) -> Result { + Ok(self.containers.remove(0)) + } + } + + fn make_container(data_type: DataType, message: HdlcEncapsulatedMessage) -> MessagesContainer { + MessagesContainer { + data_type, + num_messages: 1, + messages: vec![message], + } + } + + // this log is based on one captured on a real device -- if it fails to + // serialize or deserialize, that's probably a problem with this mock, not + // the DiagReader implementation + fn get_test_message(payload: &[u8]) -> (HdlcEncapsulatedMessage, Message) { + let length_with_payload = 31 + payload.len() as u16; + let message = Message::Log { + pending_msgs: 0, + outer_length: length_with_payload, + inner_length: length_with_payload, + log_type: 0xb0c0, + timestamp: Timestamp { ts: 72659535985485082 }, + body: LogBody::LteRrcOtaMessage { + ext_header_version: 20, + packet: LteRrcOtaPacket::V8 { + rrc_rel_maj: 14, + rrc_rel_min: 48, + bearer_id: 0, + phy_cell_id: 160, + earfcn: 2050, + sfn_subfn: 4057, + pdu_num: 5, + sib_mask: 0, + len: payload.len() as u16, + packet: payload.to_vec(), + }, + }, + }; + let serialized = message.to_bytes().expect("failed to serialize test message"); + let encapsulated_data = hdlc::hdlc_encapsulate(&serialized, &CRC_CCITT); + let encapsulated = HdlcEncapsulatedMessage { + len: encapsulated_data.len() as u32, + data: encapsulated_data, + }; + (encapsulated, message) + } + + #[test] + fn test_skipping_nonuser_containers() { + let (encapsulated1, message1) = get_test_message(&[1]); + let (encapsulated2, _) = get_test_message(&[2]); + let (encapsulated3, message3) = get_test_message(&[3]); + let mut reader = MockReader { + containers: vec![ + make_container(DataType::UserSpace, encapsulated1), + make_container(DataType::Other(0), encapsulated2), + make_container(DataType::UserSpace, encapsulated3), + ], + }; + assert_eq!(reader.read_response(), Ok(vec![Ok(message1)])); + assert_eq!(reader.read_response(), Ok(vec![Ok(message3)])); + } + + #[test] + fn test_containers_with_multiple_messages() { + let (encapsulated1, message1) = get_test_message(&[1]); + let (encapsulated2, message2) = get_test_message(&[2]); + let mut container1 = make_container(DataType::UserSpace, encapsulated1); + container1.messages.push(encapsulated2); + container1.num_messages += 1; + let (encapsulated3, message3) = get_test_message(&[3]); + let mut reader = MockReader { + containers: vec![ + container1, + make_container(DataType::UserSpace, encapsulated3), + ], + }; + assert_eq!(reader.read_response(), Ok(vec![Ok(message1), Ok(message2)])); + assert_eq!(reader.read_response(), Ok(vec![Ok(message3)])); + } + + #[test] + fn test_containers_with_concatenated_message() { + let (mut encapsulated1, message1) = get_test_message(&[1]); + let (encapsulated2, message2) = get_test_message(&[2]); + encapsulated1.data.extend(encapsulated2.data); + encapsulated1.len += encapsulated2.len; + let (encapsulated3, message3) = get_test_message(&[3]); + let mut reader = MockReader { + containers: vec![ + make_container(DataType::UserSpace, encapsulated1), + make_container(DataType::UserSpace, encapsulated3), + ], + }; + assert_eq!(reader.read_response(), Ok(vec![Ok(message1), Ok(message2)])); + assert_eq!(reader.read_response(), Ok(vec![Ok(message3)])); + } + + #[test] + fn test_handles_parsing_errors() { + let (encapsulated1, message1) = get_test_message(&[1]); + let bad_message = hdlc::hdlc_encapsulate(&[0x01, 0x02, 0x03, 0x04], &CRC_CCITT); + let encapsulated2 = HdlcEncapsulatedMessage { + len: bad_message.len() as u32, + data: bad_message, + }; + let mut container = make_container(DataType::UserSpace, encapsulated1); + container.messages.push(encapsulated2); + container.num_messages += 1; + let mut reader = MockReader { + containers: vec![container], + }; + let result = reader.read_response().unwrap(); + assert_eq!(result[0], Ok(message1)); + assert!(matches!(result[1], Err(DiagParsingError::MessageParsingError(_, _)))); + } + + #[test] + fn test_handles_encapsulation_errors() { + let (encapsulated1, message1) = get_test_message(&[1]); + let bad_encapsulation = HdlcEncapsulatedMessage { + len: 4, + data: vec![0x01, 0x02, 0x03, 0x04], + }; + let mut container = make_container(DataType::UserSpace, encapsulated1); + container.messages.push(bad_encapsulation); + container.num_messages += 1; + let mut reader = MockReader { + containers: vec![container], + }; + let result = reader.read_response().unwrap(); + assert_eq!(result[0], Ok(message1)); + assert!(matches!(result[1], Err(DiagParsingError::HdlcDecapsulationError(_, _)))); + } +} diff --git a/src/qmdl.rs b/src/qmdl.rs index d2e9f6e..c9ff5d6 100644 --- a/src/qmdl.rs +++ b/src/qmdl.rs @@ -1,60 +1,81 @@ -//! QMDL files are Qualcomm Mobile Diagnostic Logs. Their format is very simple, -//! just a series of of concatenated HDLC encapsulated diag::Message structs. +//! Qualcomm Mobile Diagnostic Log (QMDL) files have a very simple format: just +//! a series of of concatenated HDLC encapsulated diag::Message structs. +//! QmdlReader and QmdlWriter can read and write MessagesContainers to and from +//! QMDL files. use crate::diag_reader::DiagReader; use crate::diag::{MessagesContainer, MESSAGE_TERMINATOR, HdlcEncapsulatedMessage, DataType}; -use std::fs::File; -use std::io::{Write, BufReader, BufRead}; +use std::io::{Write, BufReader, BufRead, Read}; +use thiserror::Error; +use log::error; -pub struct QmdlFileWriter { - file: File, +pub struct QmdlWriter where T: Write { + writer: T, pub total_written: usize, } -impl QmdlFileWriter { - pub fn new

(path: P) -> std::io::Result where P: AsRef { - let file = std::fs::File::options() - .create(true) - .append(true) - .open(path)?; - Ok(QmdlFileWriter { - file, - total_written: 0, - }) +impl QmdlWriter where T: Write { + pub fn new(writer: T) -> Self { + QmdlWriter::new_with_existing_size(writer, 0) + } + + pub fn new_with_existing_size(writer: T, existing_size: usize) -> Self { + QmdlWriter { + writer, + total_written: existing_size, + } } pub fn write_container(&mut self, container: &MessagesContainer) -> std::io::Result<()> { for msg in &container.messages { - self.file.write_all(&msg.data)?; + self.writer.write_all(&msg.data)?; self.total_written += msg.data.len(); } Ok(()) } } -pub struct QmdlFileReader { - file: BufReader, - buf: Vec +#[derive(Debug, Error)] +pub enum QmdlReaderError { + #[error("IO error: {0}")] + IoError(#[from] std::io::Error), + #[error("Reached max_bytes count {0}")] + MaxBytesReached(usize), } -impl QmdlFileReader { - pub fn new

(path: P) -> std::io::Result where P: AsRef { - let file = std::fs::File::options() - .read(true) - .open(path)?; - Ok(QmdlFileReader { - file: BufReader::new(file), - buf: Vec::new(), - }) +pub struct QmdlReader where T: Read { + reader: BufReader, + bytes_read: usize, + max_bytes: Option, +} + +impl QmdlReader where T: Read { + pub fn new(reader: T, max_bytes: Option) -> Self { + QmdlReader { + reader: BufReader::new(reader), + bytes_read: 0, + max_bytes, + } } } -impl DiagReader for QmdlFileReader { - type Err = std::io::Error; +impl DiagReader for QmdlReader where T: Read { + type Err = QmdlReaderError; - fn get_next_messages_container(&mut self) -> std::io::Result { - let bytes_read = self.file.read_until(MESSAGE_TERMINATOR, &mut self.buf)?; + fn get_next_messages_container(&mut self) -> Result { + if let Some(max_bytes) = self.max_bytes { + if self.bytes_read >= max_bytes { + if self.bytes_read > max_bytes { + error!("warning: {} bytes read, but max_bytes was {}", self.bytes_read, max_bytes); + } + return Err(QmdlReaderError::MaxBytesReached(max_bytes)); + } + } + + let mut buf = Vec::new(); + let bytes_read = self.reader.read_until(MESSAGE_TERMINATOR, &mut buf)?; + self.bytes_read += bytes_read; // Since QMDL is just a flat list of messages, we can't actually // reproduce the container structure they came from in the original @@ -66,10 +87,126 @@ impl DiagReader for QmdlFileReader { num_messages: 1, messages: vec![ HdlcEncapsulatedMessage { - len: 1, - data: self.buf[0..bytes_read].to_vec(), + len: bytes_read as u32, + data: buf, }, ] }) } } + +#[cfg(test)] +mod test { + use std::io::Cursor; + + use crate::hdlc::hdlc_encapsulate; + use crate::diag_reader::CRC_CCITT; + + use super::*; + + fn get_test_messages() -> Vec { + let messages: Vec = (10..20).map(|i| { + let data = hdlc_encapsulate(&vec![i as u8; i], &CRC_CCITT); + HdlcEncapsulatedMessage { + len: data.len() as u32, + data, + } + }).collect(); + messages + } + + // returns a byte array consisting of concatenated HDLC encapsulated + // test messages + fn get_test_message_bytes() -> Vec { + get_test_messages().iter() + .flat_map(|msg| msg.data.clone()) + .collect() + } + + fn get_test_containers() -> Vec { + let messages = get_test_messages(); + let (messages1, messages2) = messages.split_at(5); + vec![ + MessagesContainer { + data_type: DataType::UserSpace, + num_messages: messages1.len() as u32, + messages: messages1.to_vec(), + }, + MessagesContainer { + data_type: DataType::UserSpace, + num_messages: messages2.len() as u32, + messages: messages2.to_vec() + }, + ] + } + + #[test] + fn test_unbounded_qmdl_reader() { + let mut buf = Cursor::new(get_test_message_bytes()); + let mut reader = QmdlReader::new(&mut buf, None); + let expected_messages = get_test_messages(); + for message in expected_messages { + let expected_container = MessagesContainer { + data_type: DataType::UserSpace, + num_messages: 1, + messages: vec![message], + }; + assert_eq!(expected_container, reader.get_next_messages_container().unwrap()); + } + } + + #[test] + fn test_bounded_qmdl_reader() { + let mut buf = Cursor::new(get_test_message_bytes()); + + // bound the reader to the first two messages + let mut expected_messages = get_test_messages(); + let limit = expected_messages[0].len + expected_messages[1].len; + + let mut reader = QmdlReader::new(&mut buf, Some(limit as usize)); + for message in expected_messages.drain(0..2) { + let expected_container = MessagesContainer { + data_type: DataType::UserSpace, + num_messages: 1, + messages: vec![message], + }; + assert_eq!(expected_container, reader.get_next_messages_container().unwrap()); + } + assert!(matches!(reader.get_next_messages_container(), Err(QmdlReaderError::MaxBytesReached(_)))); + } + + #[test] + fn test_qmdl_writer() { + let mut buf = Vec::new(); + let mut writer = QmdlWriter::new(&mut buf); + let expected_containers = get_test_containers(); + for container in &expected_containers { + writer.write_container(container).unwrap(); + } + assert_eq!(writer.total_written, buf.len()); + assert_eq!(buf, get_test_message_bytes()); + } + + #[test] + fn test_writing_and_reading() { + let mut buf = Vec::new(); + let mut writer = QmdlWriter::new(&mut buf); + let expected_containers = get_test_containers(); + for container in &expected_containers { + writer.write_container(container).unwrap(); + } + + let limit = Some(buf.len()); + let mut reader = QmdlReader::new(Cursor::new(&mut buf), limit); + let expected_messages = get_test_messages(); + for message in expected_messages { + let expected_container = MessagesContainer { + data_type: DataType::UserSpace, + num_messages: 1, + messages: vec![message], + }; + assert_eq!(expected_container, reader.get_next_messages_container().unwrap()); + } + assert!(matches!(reader.get_next_messages_container(), Err(QmdlReaderError::MaxBytesReached(_)))); + } +} From ed1848def6c63ee8b6474e0ec9eeb60c44b5393a Mon Sep 17 00:00:00 2001 From: Will Greenberg Date: Wed, 27 Dec 2023 15:45:00 -0800 Subject: [PATCH 05/16] minor whitespace fixes --- tests/test_lte_parsing.rs | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/tests/test_lte_parsing.rs b/tests/test_lte_parsing.rs index e79d951..859e5f3 100644 --- a/tests/test_lte_parsing.rs +++ b/tests/test_lte_parsing.rs @@ -12,7 +12,11 @@ use deku::prelude::*; #[test] fn test_lte_rrc_ota() { let mut parser = GsmtapParser::new(); - let v26_binary = &[0x10, 0x0, 0x23, 0x0, 0x23, 0x0, 0xc0, 0xb0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1a, 0xf, 0x40, 0xf, 0x40, 0x1, 0xe, 0x1, 0x13, 0x7, 0x0, 0x0, 0x0, 0x0, 0xb, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x10, 0x15]; + let v26_binary = &[ + 0x10, 0x0, 0x23, 0x0, 0x23, 0x0, 0xc0, 0xb0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x1a, 0xf, 0x40, 0xf, 0x40, 0x1, 0xe, 0x1, 0x13, 0x7, + 0x0, 0x0, 0x0, 0x0, 0xb, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x10, 0x15 + ]; let (_, parsed) = Message::from_bytes((v26_binary, 0)).unwrap(); assert_eq!(&parsed, &Message::Log { pending_msgs: 0, @@ -84,7 +88,6 @@ fn test_lte_rrc_ota() { let (_, gsmtap_msg) = parser.recv_message(parsed).unwrap().unwrap(); assert_eq!(&gsmtap_msg.payload, &[ 0x10, 0x15, - ]); assert_eq!(gsmtap_msg.header.packet_type, 13); assert_eq!(gsmtap_msg.header.timeslot, 0); @@ -102,7 +105,6 @@ fn test_lte_rrc_ota() { 0x00, 0x00, 0x09, 0xdc, 0x05, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x40, 0x85, 0x8e, 0xc4, 0xe5, 0xbf, 0xe0, 0x50, 0xdc, 0x29, 0x15, 0x16, 0x00, - ]; let (_, parsed) = Message::from_bytes((v24_binary, 0)).unwrap(); assert_eq!(&parsed, &Message::Log { @@ -123,7 +125,10 @@ fn test_lte_rrc_ota() { pdu_num: 5, sib_mask: 0, len: 13, - packet: vec![0x40, 0x85, 0x8e, 0xc4, 0xe5, 0xbf, 0xe0, 0x50, 0xdc, 0x29, 0x15, 0x16, 0x0], + packet: vec![ + 0x40, 0x85, 0x8e, 0xc4, 0xe5, 0xbf, 0xe0, 0x50, 0xdc, 0x29, + 0x15, 0x16, 0x0 + ], }, }, }); @@ -131,7 +136,6 @@ fn test_lte_rrc_ota() { assert_eq!(&gsmtap_msg.payload, &[ 0x40, 0x85, 0x8e, 0xc4, 0xe5, 0xbf, 0xe0, 0x50, 0xdc, 0x29, 0x15, 0x16, 0x00, - ]); assert_eq!(gsmtap_msg.header.packet_type, 13); assert_eq!(gsmtap_msg.header.timeslot, 0); @@ -171,7 +175,11 @@ fn test_lte_rrc_ota() { pdu_num: 9, sib_mask: 0, len: 24, - packet: vec![0x8, 0x10, 0xa7, 0x14, 0x53, 0x59, 0xa6, 0x5, 0x43, 0x68, 0xc0, 0x3b, 0xda, 0x30, 0x4, 0xa6, 0x88, 0x2, 0x8d, 0xa2, 0x0, 0x9a, 0x68, 0x40], + packet: vec![ + 0x8, 0x10, 0xa7, 0x14, 0x53, 0x59, 0xa6, 0x5, 0x43, 0x68, + 0xc0, 0x3b, 0xda, 0x30, 0x4, 0xa6, 0x88, 0x2, 0x8d, 0xa2, + 0x0, 0x9a, 0x68, 0x40 + ], }, }, }); @@ -269,7 +277,6 @@ fn test_lte_rrc_ota() { let (_, gsmtap_msg) = parser.recv_message(parsed).unwrap().unwrap(); assert_eq!(&gsmtap_msg.payload, &[ 0x40, 0x0c, 0x8e, 0xc9, 0x42, 0x89, 0xe0, - ]); assert_eq!(gsmtap_msg.header.packet_type, 13); assert_eq!(gsmtap_msg.header.timeslot, 0); @@ -289,7 +296,6 @@ fn test_lte_rrc_ota() { 0x41, 0xa3, 0x1c, 0x31, 0x68, 0x04, 0x40, 0x1a, 0x00, 0x49, 0x16, 0x7c, 0x23, 0x15, 0x9f, 0x00, 0x10, 0x67, 0xc1, 0x06, 0xd9, 0xe0, 0x00, - ]; let (_, parsed) = Message::from_bytes((v15_binary, 0)).unwrap(); assert_eq!(&parsed, &Message::Log { @@ -310,7 +316,11 @@ fn test_lte_rrc_ota() { pdu_num: 9, sib_mask: 0, len: 28, - packet: vec![0x8, 0x10, 0xa5, 0x34, 0x61, 0x41, 0xa3, 0x1c, 0x31, 0x68, 0x4, 0x40, 0x1a, 0x0, 0x49, 0x16, 0x7c, 0x23, 0x15, 0x9f, 0x0, 0x10, 0x67, 0xc1, 0x6, 0xd9, 0xe0, 0x0], + packet: vec![ + 0x8, 0x10, 0xa5, 0x34, 0x61, 0x41, 0xa3, 0x1c, 0x31, 0x68, + 0x4, 0x40, 0x1a, 0x0, 0x49, 0x16, 0x7c, 0x23, 0x15, 0x9f, + 0x0, 0x10, 0x67, 0xc1, 0x6, 0xd9, 0xe0, 0x0 + ], }, }, }); @@ -484,7 +494,10 @@ fn test_lte_rrc_ota() { pdu_num: 2, sib_mask: 2, len: 18, - packet: vec![0x40, 0x49, 0x88, 0x5, 0xc0, 0x97, 0x2, 0xd3, 0xb0, 0x98, 0x1c, 0x20, 0xa0, 0x81, 0x8c, 0x43, 0x26, 0xd0], + packet: vec![ + 0x40, 0x49, 0x88, 0x5, 0xc0, 0x97, 0x2, 0xd3, 0xb0, 0x98, + 0x1c, 0x20, 0xa0, 0x81, 0x8c, 0x43, 0x26, 0xd0 + ], }, }, }); From f3afda3ce48c7ef54648eea6975f80617e44375d Mon Sep 17 00:00:00 2001 From: Will Greenberg Date: Wed, 27 Dec 2023 15:45:19 -0800 Subject: [PATCH 06/16] pcap: refactor to take Write, rename struct --- src/pcap.rs | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/pcap.rs b/src/pcap.rs index 66cf7ba..2f90bd3 100644 --- a/src/pcap.rs +++ b/src/pcap.rs @@ -1,9 +1,8 @@ use crate::gsmtap::GsmtapMessage; use crate::diag::Timestamp; -use std::fs::File; +use std::io::Write; use std::borrow::Cow; -use std::path::Path; use chrono::prelude::*; use deku::prelude::*; use pcap_file::pcapng::blocks::enhanced_packet::EnhancedPacketBlock; @@ -13,7 +12,7 @@ use pcap_file::PcapError; use thiserror::Error; #[derive(Error, Debug)] -pub enum PcapFileError { +pub enum GsmtapPcapError { #[error("IO error: {0}")] Io(#[from] std::io::Error), #[error("Pcap error: {0}")] @@ -22,8 +21,8 @@ pub enum PcapFileError { Deku(#[from] DekuError), } -pub struct PcapFile { - writer: PcapNgWriter, +pub struct GsmtapPcapWriter where T: Write { + writer: PcapNgWriter, ip_id: u16, } @@ -55,17 +54,13 @@ struct UdpHeader { checksum: u16, } -impl PcapFile { - pub fn new

(path: P) -> Result where P: AsRef { - let file = std::fs::File::options() - .create(true) - .write(true) - .open(path)?; - let writer = PcapNgWriter::new(file)?; - Ok(PcapFile { writer, ip_id: 0 }) +impl GsmtapPcapWriter where T: Write { + pub fn new(writer: T) -> Result { + let writer = PcapNgWriter::new(writer)?; + Ok(GsmtapPcapWriter { writer, ip_id: 0 }) } - pub fn write_iface_header(&mut self) -> Result<(), PcapFileError> { + pub fn write_iface_header(&mut self) -> Result<(), GsmtapPcapError> { let interface = InterfaceDescriptionBlock { linktype: pcap_file::DataLink::IPV4, snaplen: 0xffff, @@ -75,7 +70,7 @@ impl PcapFile { Ok(()) } - pub fn write_gsmtap_message(&mut self, msg: GsmtapMessage, timestamp: Timestamp) -> Result<(), PcapFileError> { + pub fn write_gsmtap_message(&mut self, msg: GsmtapMessage, timestamp: Timestamp) -> Result<(), GsmtapPcapError> { let time_since_epoch = timestamp.to_datetime().signed_duration_since(DateTime::UNIX_EPOCH); let secs_since_epoch = time_since_epoch.num_seconds() as u64; let nsecs_since_epoch = time_since_epoch.num_nanoseconds().unwrap_or(0) as u32; From 2a27f04cc18140d09b199f0f66cd72c43ea556ff Mon Sep 17 00:00:00 2001 From: Will Greenberg Date: Wed, 27 Dec 2023 15:45:49 -0800 Subject: [PATCH 07/16] wavehunter_reader: update to new interfaces --- src/bin/wavehunter_reader.rs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/bin/wavehunter_reader.rs b/src/bin/wavehunter_reader.rs index 470d054..432f36e 100644 --- a/src/bin/wavehunter_reader.rs +++ b/src/bin/wavehunter_reader.rs @@ -1,7 +1,9 @@ -use wavehunter::qmdl::QmdlFileReader; +use std::fs::File; + +use wavehunter::qmdl::QmdlReader; use wavehunter::diag_reader::DiagReader; use wavehunter::gsmtap_parser::GsmtapParser; -use wavehunter::pcap::PcapFile; +use wavehunter::pcap::GsmtapPcapWriter; use log::{debug, error}; @@ -13,11 +15,18 @@ fn main() { error!("Usage: {} /path/to/qmdl/file", args[0]); std::process::exit(1); } - let mut qmdl_reader = QmdlFileReader::new(&args[1]).unwrap(); + + let qmdl_file = File::open(&args[1]).unwrap(); + let mut qmdl_reader = QmdlReader::new(qmdl_file, None); let mut gsmtap_parser = GsmtapParser::new(); - let mut pcap_file = PcapFile::new("./wavehunter.pcap").unwrap(); - pcap_file.write_iface_header().unwrap(); + let pcap_file = std::fs::File::options() + .create(true) + .write(true) + .open("./wavehunter.pcap") + .expect("error opening pcap file"); + let mut pcap_writer = GsmtapPcapWriter::new(pcap_file).unwrap(); + pcap_writer.write_iface_header().unwrap(); loop { for maybe_msg in qmdl_reader.read_response().expect("error reading qmdl file") { @@ -27,7 +36,7 @@ fn main() { let maybe_gsmtap_msg = gsmtap_parser.recv_message(msg).expect("error parsing gsmtap message"); if let Some((timestamp, gsmtap_msg)) = maybe_gsmtap_msg { debug!("gsmtap_msg: {:?}", gsmtap_msg); - pcap_file.write_gsmtap_message(gsmtap_msg, timestamp) + pcap_writer.write_gsmtap_message(gsmtap_msg, timestamp) .expect("error writing pcap packet"); } }, From 51fc9eba56c865001fb5376b49013125e3806dac Mon Sep 17 00:00:00 2001 From: Will Greenberg Date: Wed, 27 Dec 2023 15:46:00 -0800 Subject: [PATCH 08/16] wavehunter: implement HTTP server The server streams a pcap file to the user in small chunks, so the memory overhead is extremely small. --- Cargo.lock | 617 +++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 5 +- src/bin/wavehunter.rs | 223 ++++++++++++--- 3 files changed, 800 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7e46315..cccf36d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "aho-corasick" version = "1.1.2" @@ -26,12 +41,120 @@ dependencies = [ "libc", ] +[[package]] +name = "async-trait" +version = "0.1.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf6721fb0140e4f897002dd086c06f6c27775df19cfe1fccb21181a48fd2c98" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "axum" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202651474fe73c62d9e0a56c6133f7a0ff1dc1c8cf7a5b03381af2a26553ac9d" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77cb22c689c44d4c07b0ab44ebc25d69d8ae601a2f28fb8d672d344178fa17aa" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-extra" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523ae92256049a3b02d3bb4df80152386cd97ddba0c8c5077619bdc8c4b1859b" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "serde", + "tokio", + "tokio-util", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.4.1" @@ -232,12 +355,85 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + [[package]] name = "funty" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "h2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d308f63daf4181410c242d34c11f928dcb3aa105852019e043c9d1f4e4368a" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.14.3" @@ -250,12 +446,95 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +[[package]] +name = "http" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "humantime" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "hyper" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5aa53871fc917b1a9ed87b683a5d86db645e23acb32c2e0785a353e522fb75" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdea9aac0dbe5a9240d68cfd9501e2db94222c6dc06843e06640b9e07f0fdc67" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tracing", +] + [[package]] name = "iana-time-zone" version = "0.1.58" @@ -306,6 +585,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + [[package]] name = "js-sys" version = "0.3.66" @@ -327,18 +612,60 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "memchr" version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "num-traits" version = "0.2.17" @@ -348,12 +675,54 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + [[package]] name = "pcap-file" version = "2.0.0" @@ -365,6 +734,44 @@ dependencies = [ "thiserror", ] +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -399,6 +806,15 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "regex" version = "1.10.2" @@ -428,19 +844,43 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + [[package]] name = "rustix" version = "0.38.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9470c4bf8246c8daf25f9598dca807fb6510347b1e1cfa55749113850c79d88a" dependencies = [ - "bitflags", + "bitflags 2.4.1", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.193" @@ -461,6 +901,27 @@ dependencies = [ "syn 2.0.39", ] +[[package]] +name = "serde_json" +version = "1.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_spanned" version = "0.6.5" @@ -470,6 +931,52 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "strsim" version = "0.10.0" @@ -498,6 +1005,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "tap" version = "1.0.1" @@ -533,6 +1046,50 @@ dependencies = [ "syn 2.0.39", ] +[[package]] +name = "tokio" +version = "1.35.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + [[package]] name = "toml" version = "0.8.8" @@ -578,12 +1135,66 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "wasm-bindgen" version = "0.2.89" @@ -642,16 +1253,20 @@ checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" name = "wavehunter" version = "0.1.0" dependencies = [ + "axum", + "axum-extra", "bytes", "chrono", "crc", "deku", "env_logger", + "futures-core", "libc", "log", "pcap-file", "serde", "thiserror", + "tokio", "toml", ] diff --git a/Cargo.toml b/Cargo.toml index 54aeec0..809d8f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,4 +30,7 @@ thiserror = "1.0.50" # TODO: split into binary-only dependencies toml = "0.8.8" serde = { version = "1.0.193", features = ["derive"] } - +tokio = { version = "1.35.1", features = ["full"] } +axum = "0.7.2" +axum-extra = { version = "0.9.0", features = ["async-read-body"] } +futures-core = "0.3.30" diff --git a/src/bin/wavehunter.rs b/src/bin/wavehunter.rs index 5a014c2..6e106c9 100644 --- a/src/bin/wavehunter.rs +++ b/src/bin/wavehunter.rs @@ -1,11 +1,28 @@ +use axum::body::Body; +use axum::http::header::CONTENT_TYPE; +use futures_core::Stream; +use log::error; use wavehunter::diag_device::{DiagDevice, DiagDeviceError}; use wavehunter::diag_reader::DiagReader; -use wavehunter::gsmtap_parser::{GsmtapParser, GsmtapParserError}; -use wavehunter::pcap::{PcapFile, PcapFileError}; +use wavehunter::gsmtap_parser::GsmtapParser; +use wavehunter::pcap::GsmtapPcapWriter; -use log::debug; +use axum::Router; +use axum::extract::State; +use axum::http::StatusCode; +use axum::response::{Response, IntoResponse}; +use axum::routing::get; +use std::fs::File; use thiserror::Error; use serde::Deserialize; +use wavehunter::qmdl::{QmdlReader, QmdlReaderError}; +use std::io::Write; +use std::sync::Arc; +use std::net::SocketAddr; +use std::pin::Pin; +use std::task::{Poll, Context}; +use tokio::net::TcpListener; +use tokio::sync::{mpsc, RwLock}; use toml; #[derive(Error, Debug)] @@ -14,28 +31,33 @@ enum WavehunterError { MissingConfigFile(String), #[error("Config file parsing error: {0}")] ConfigFileParsingError(#[from] toml::de::Error), - #[error("Pcap file initialization error: {0}")] - PcapFileInitError(PcapFileError), - #[error("Pcap file write error: {0}")] - PcapFileWriteError(PcapFileError), #[error("Diag intialization error: {0}")] DiagInitError(DiagDeviceError), #[error("Diag read error: {0}")] DiagReadError(DiagDeviceError), - #[error("GSMTAP parsing error: {0}")] - GsmtapParsingError(GsmtapParserError), + #[error("Tokio error: {0}")] + TokioError(#[from] tokio::io::Error), } #[derive(Deserialize)] struct ConfigFile { qmdl_path: Option, - pcap_path: Option, + port: Option, } #[derive(Debug)] struct Config { qmdl_path: String, - pcap_path: String, + port: u16, +} + +impl Default for Config { + fn default() -> Self { + Config { + qmdl_path: "./wavehunter.qmdl".to_string(), + port: 8080, + } + } } fn parse_config

(path: P) -> Result where P: AsRef { @@ -43,54 +65,169 @@ fn parse_config

(path: P) -> Result where P: AsRef Result<(), WavehunterError> { - env_logger::init(); +struct Args { + config_path: String, +} +fn parse_args() -> Args { let args: Vec = std::env::args().collect(); if args.len() != 2 { println!("Usage: {} /path/to/config/file", args[0]); std::process::exit(1); } + Args { + config_path: args[1].clone(), + } +} - let config = parse_config(&args[1])?; +fn run_diag_read_thread(mut dev: DiagDevice, bytes_read_lock: Arc>) -> tokio::task::JoinHandle> { + tokio::task::spawn_blocking(move || { + loop { + // TODO: once we're actually doing analysis, we'll wanna use the messages + // returned here. Until then, the DiagDevice has already written those messages + // to the QMDL file, so we can just ignore them. + let _messages = dev.read_response().map_err(WavehunterError::DiagReadError)?; + + // keep track of how many bytes were written to the QMDL file so we can read + // a valid block of data from it in the HTTP server + let mut bytes_read = bytes_read_lock.blocking_write(); + *bytes_read = dev.qmdl_writer.total_written; + } + }) +} + +// Streams a pcap file chunk-by-chunk to the client by reading the QMDL data +// written so far. This is done by spawning a blocking thread (a tokio thread +// capable of handling blocking operations) which streams chunks of pcap data to +// a channel that's piped to the client. +async fn serve_pcap(State(state): State>) -> Result { + let qmdl_bytes_written = *state.qmdl_bytes_written.read().await; + if qmdl_bytes_written == 0 { + return Err(( + StatusCode::SERVICE_UNAVAILABLE, + "QMDL file is empty, try again in a bit!".to_string() + )); + } + + let (tx, rx) = mpsc::channel(1); + let channel_reader = ChannelReader { rx }; + let channel_writer = ChannelWriter { tx }; + tokio::task::spawn_blocking(move || { + // the QMDL reader should stop at the last successfully written data + // chunk (qmdl_bytes_written) + let qmdl_file = File::open(&state.qmdl_path).unwrap(); + let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(qmdl_bytes_written)); + + let mut gsmtap_parser = GsmtapParser::new(); + let mut pcap_writer = GsmtapPcapWriter::new(channel_writer).unwrap(); + pcap_writer.write_iface_header().unwrap(); + loop { + match qmdl_reader.read_response() { + Ok(messages) => { + for maybe_msg in messages { + match maybe_msg { + Ok(msg) => { + let maybe_gsmtap_msg = gsmtap_parser.recv_message(msg) + .expect("error parsing gsmtap message"); + if let Some((timestamp, gsmtap_msg)) = maybe_gsmtap_msg { + pcap_writer.write_gsmtap_message(gsmtap_msg, timestamp) + .expect("error writing pcap packet"); + } + }, + Err(e) => { + error!("error parsing message: {:?}", e); + }, + } + } + }, + // this is expected, and just means we've reached the end of the + // safely written QMDL data + Err(QmdlReaderError::MaxBytesReached(_)) => break, + Err(e) => { + error!("error reading qmdl file: {:?}", e); + break; + }, + } + } + }); + + let headers = [(CONTENT_TYPE, "application/vnd.tcpdump.pcap")]; + let body = Body::from_stream(channel_reader); + Ok((headers, body).into_response()) +} + +struct ChannelWriter { + tx: mpsc::Sender>, +} + +impl Write for ChannelWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.tx.blocking_send(buf.to_vec()) + .map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, "channel closed"))?; + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +struct ChannelReader { + rx: mpsc::Receiver>, +} + +impl Stream for ChannelReader { + type Item = Result, String>; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.rx.poll_recv(cx) { + Poll::Ready(Some(msg)) => Poll::Ready(Some(Ok(msg))), + Poll::Ready(None) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + } + } +} + +struct ServerState { + qmdl_bytes_written: Arc>, + qmdl_path: String, +} + +#[tokio::main] +async fn main() -> Result<(), WavehunterError> { + env_logger::init(); + + let args = parse_args(); + let config = parse_config(&args.config_path)?; let mut dev = DiagDevice::new(&config.qmdl_path) .map_err(WavehunterError::DiagInitError)?; dev.config_logs() .map_err(WavehunterError::DiagInitError)?; + let qmdl_bytes_lock = Arc::new(RwLock::new(dev.qmdl_writer.total_written)); + // TODO: handle exiting gracefully + let _read_thread_handle = run_diag_read_thread(dev, qmdl_bytes_lock.clone()); + println!("The orca is hunting for stingrays..."); - let mut gsmtap_parser = GsmtapParser::new(); - // We are going to want to add a timestamp to this pcap file eventually - let mut pcap_file = PcapFile::new(&config.pcap_path) - .map_err(WavehunterError::PcapFileInitError)?; - pcap_file.write_iface_header() - .map_err(WavehunterError::PcapFileWriteError)?; + let addr = SocketAddr::from(([127, 0, 0, 1], config.port)); + let listener = TcpListener::bind(&addr).await?; + let state = Arc::new(ServerState { + qmdl_bytes_written: qmdl_bytes_lock, + qmdl_path: config.qmdl_path, + }); - loop { - for maybe_msg in dev.read_response().map_err(WavehunterError::DiagReadError)? { - match maybe_msg { - Ok(msg) => { - debug!("msg: {:?}", msg); - let maybe_gsmtap_msg = gsmtap_parser.recv_message(msg) - .map_err(WavehunterError::GsmtapParsingError)?; - if let Some((timestamp, gsmtap_msg)) = maybe_gsmtap_msg { - debug!("gsmtap_msg: {:?}", gsmtap_msg); - pcap_file.write_gsmtap_message(gsmtap_msg, timestamp) - .map_err(WavehunterError::PcapFileWriteError)?; - } - }, - Err(e) => { - dbg!("error parsing message: {:?}", e); - }, - } - } - } + let app = Router::new() + .route("/output.pcap", get(serve_pcap)) + .with_state(state); + axum::serve(listener, app).await.unwrap(); + + Ok(()) } From e20fe92213e55e4aa02a0b2ea0998d90d2125e6b Mon Sep 17 00:00:00 2001 From: Will Greenberg Date: Wed, 27 Dec 2023 16:08:37 -0800 Subject: [PATCH 09/16] Refactor lib/binary into separate crates --- Cargo.lock | 96 ++++++++----------- Cargo.toml | 41 ++------ orca/Cargo.toml | 16 ++++ {src => orca/src}/diag.rs | 0 {src => orca/src}/diag_device.rs | 0 {src => orca/src}/diag_reader.rs | 0 {src => orca/src}/gsmtap.rs | 0 {src => orca/src}/gsmtap_parser.rs | 0 {src => orca/src}/hdlc.rs | 0 {src => orca/src}/lib.rs | 0 {src => orca/src}/log_codes.rs | 0 {src => orca/src}/pcap.rs | 0 {src => orca/src}/qmdl.rs | 0 {tests => orca/tests}/test_lte_parsing.rs | 0 src/bin/wavehunter_reader.rs | 49 ---------- wavehunter/Cargo.toml | 17 ++++ .../wavehunter.rs => wavehunter/src/main.rs | 0 17 files changed, 80 insertions(+), 139 deletions(-) create mode 100644 orca/Cargo.toml rename {src => orca/src}/diag.rs (100%) rename {src => orca/src}/diag_device.rs (100%) rename {src => orca/src}/diag_reader.rs (100%) rename {src => orca/src}/gsmtap.rs (100%) rename {src => orca/src}/gsmtap_parser.rs (100%) rename {src => orca/src}/hdlc.rs (100%) rename {src => orca/src}/lib.rs (100%) rename {src => orca/src}/log_codes.rs (100%) rename {src => orca/src}/pcap.rs (100%) rename {src => orca/src}/qmdl.rs (100%) rename {tests => orca/tests}/test_lte_parsing.rs (100%) delete mode 100644 src/bin/wavehunter_reader.rs create mode 100644 wavehunter/Cargo.toml rename src/bin/wavehunter.rs => wavehunter/src/main.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index cccf36d..80765a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,7 +49,7 @@ checksum = "fdf6721fb0140e4f897002dd086c06f6c27775df19cfe1fccb21181a48fd2c98" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.43", ] [[package]] @@ -111,29 +111,6 @@ dependencies = [ "tower-service", ] -[[package]] -name = "axum-extra" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523ae92256049a3b02d3bb4df80152386cd97ddba0c8c5077619bdc8c4b1859b" -dependencies = [ - "axum", - "axum-core", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "serde", - "tokio", - "tokio-util", - "tower", - "tower-layer", - "tower-service", -] - [[package]] name = "backtrace" version = "0.3.69" @@ -602,9 +579,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.150" +version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" [[package]] name = "linux-raw-sys" @@ -657,9 +634,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "wasi", @@ -696,9 +673,24 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "orca" +version = "0.1.0" +dependencies = [ + "bytes", + "chrono", + "crc", + "deku", + "env_logger", + "libc", + "log", + "pcap-file", + "thiserror", +] [[package]] name = "parking_lot" @@ -757,7 +749,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.43", ] [[package]] @@ -784,9 +776,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8" dependencies = [ "unicode-ident", ] @@ -852,9 +844,9 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.26" +version = "0.38.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9470c4bf8246c8daf25f9598dca807fb6510347b1e1cfa55749113850c79d88a" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" dependencies = [ "bitflags 2.4.1", "errno", @@ -898,7 +890,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.43", ] [[package]] @@ -996,9 +988,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.39" +version = "2.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "ee659fb5f3d355364e1f3e5bc10fb82068efbf824a1e9d1c9504244a6469ad53" dependencies = [ "proc-macro2", "quote", @@ -1028,22 +1020,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.50" +version = "1.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "83a48fd946b02c0a526b2e9481c8e2a17755e47039164a86c4070446e3a4614d" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "1.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "e7fbe9b594d6568a6a1443250a7e67d80b74e1e96f6d1715e1e21cc1888291d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.43", ] [[package]] @@ -1073,7 +1065,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.43", ] [[package]] @@ -1216,7 +1208,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.43", "wasm-bindgen-shared", ] @@ -1238,7 +1230,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.43", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1254,16 +1246,10 @@ name = "wavehunter" version = "0.1.0" dependencies = [ "axum", - "axum-extra", - "bytes", - "chrono", - "crc", - "deku", "env_logger", "futures-core", - "libc", "log", - "pcap-file", + "orca", "serde", "thiserror", "tokio", @@ -1444,9 +1430,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.19" +version = "0.5.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" +checksum = "97a4882e6b134d6c28953a387571f1acdd3496830d5e36c5e3a1075580ea641c" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 809d8f2..a019f56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,36 +1,7 @@ -[package] -name = "wavehunter" -version = "0.1.0" -edition = "2021" +[workspace] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[lib] -name = "wavehunter" -path = "src/lib.rs" - -[[bin]] -name = "wavehunter-reader" -path = "src/bin/wavehunter_reader.rs" - -[[bin]] -name = "wavehunter" -path = "src/bin/wavehunter.rs" - -[dependencies] -bytes = "1.5.0" -chrono = "0.4.31" -crc = "3.0.1" -deku = { version = "0.16.0", features = ["logging"] } -env_logger = "0.10.1" -libc = "0.2.150" -log = "0.4.20" -pcap-file = "2.0.0" -thiserror = "1.0.50" -# TODO: split into binary-only dependencies -toml = "0.8.8" -serde = { version = "1.0.193", features = ["derive"] } -tokio = { version = "1.35.1", features = ["full"] } -axum = "0.7.2" -axum-extra = { version = "0.9.0", features = ["async-read-body"] } -futures-core = "0.3.30" +members = [ + "orca", + "wavehunter", +] +resolver = "2" diff --git a/orca/Cargo.toml b/orca/Cargo.toml new file mode 100644 index 0000000..085ee12 --- /dev/null +++ b/orca/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "orca" +version = "0.1.0" +edition = "2021" +description = "Orbic Realtime Cellular Analysis" + +[dependencies] +bytes = "1.5.0" +chrono = "0.4.31" +crc = "3.0.1" +deku = { version = "0.16.0", features = ["logging"] } +env_logger = "0.10.1" +libc = "0.2.150" +log = "0.4.20" +pcap-file = "2.0.0" +thiserror = "1.0.50" diff --git a/src/diag.rs b/orca/src/diag.rs similarity index 100% rename from src/diag.rs rename to orca/src/diag.rs diff --git a/src/diag_device.rs b/orca/src/diag_device.rs similarity index 100% rename from src/diag_device.rs rename to orca/src/diag_device.rs diff --git a/src/diag_reader.rs b/orca/src/diag_reader.rs similarity index 100% rename from src/diag_reader.rs rename to orca/src/diag_reader.rs diff --git a/src/gsmtap.rs b/orca/src/gsmtap.rs similarity index 100% rename from src/gsmtap.rs rename to orca/src/gsmtap.rs diff --git a/src/gsmtap_parser.rs b/orca/src/gsmtap_parser.rs similarity index 100% rename from src/gsmtap_parser.rs rename to orca/src/gsmtap_parser.rs diff --git a/src/hdlc.rs b/orca/src/hdlc.rs similarity index 100% rename from src/hdlc.rs rename to orca/src/hdlc.rs diff --git a/src/lib.rs b/orca/src/lib.rs similarity index 100% rename from src/lib.rs rename to orca/src/lib.rs diff --git a/src/log_codes.rs b/orca/src/log_codes.rs similarity index 100% rename from src/log_codes.rs rename to orca/src/log_codes.rs diff --git a/src/pcap.rs b/orca/src/pcap.rs similarity index 100% rename from src/pcap.rs rename to orca/src/pcap.rs diff --git a/src/qmdl.rs b/orca/src/qmdl.rs similarity index 100% rename from src/qmdl.rs rename to orca/src/qmdl.rs diff --git a/tests/test_lte_parsing.rs b/orca/tests/test_lte_parsing.rs similarity index 100% rename from tests/test_lte_parsing.rs rename to orca/tests/test_lte_parsing.rs diff --git a/src/bin/wavehunter_reader.rs b/src/bin/wavehunter_reader.rs deleted file mode 100644 index 432f36e..0000000 --- a/src/bin/wavehunter_reader.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::fs::File; - -use wavehunter::qmdl::QmdlReader; -use wavehunter::diag_reader::DiagReader; -use wavehunter::gsmtap_parser::GsmtapParser; -use wavehunter::pcap::GsmtapPcapWriter; - -use log::{debug, error}; - -fn main() { - env_logger::init(); - - let args: Vec = std::env::args().collect(); - if args.len() != 2 { - error!("Usage: {} /path/to/qmdl/file", args[0]); - std::process::exit(1); - } - - let qmdl_file = File::open(&args[1]).unwrap(); - let mut qmdl_reader = QmdlReader::new(qmdl_file, None); - - let mut gsmtap_parser = GsmtapParser::new(); - let pcap_file = std::fs::File::options() - .create(true) - .write(true) - .open("./wavehunter.pcap") - .expect("error opening pcap file"); - let mut pcap_writer = GsmtapPcapWriter::new(pcap_file).unwrap(); - pcap_writer.write_iface_header().unwrap(); - - loop { - for maybe_msg in qmdl_reader.read_response().expect("error reading qmdl file") { - match maybe_msg { - Ok(msg) => { - debug!("msg: {:?}", msg); - let maybe_gsmtap_msg = gsmtap_parser.recv_message(msg).expect("error parsing gsmtap message"); - if let Some((timestamp, gsmtap_msg)) = maybe_gsmtap_msg { - debug!("gsmtap_msg: {:?}", gsmtap_msg); - pcap_writer.write_gsmtap_message(gsmtap_msg, timestamp) - .expect("error writing pcap packet"); - } - }, - Err(e) => { - dbg!("error parsing message: {:?}", e); - }, - } - } - } -} diff --git a/wavehunter/Cargo.toml b/wavehunter/Cargo.toml new file mode 100644 index 0000000..914a8df --- /dev/null +++ b/wavehunter/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "wavehunter" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +orca = { path = "../orca" } +toml = "0.8.8" +serde = { version = "1.0.193", features = ["derive"] } +tokio = { version = "1.35.1", features = ["full"] } +axum = "0.7.2" +futures-core = "0.3.30" +thiserror = "1.0.52" +log = "0.4.20" +env_logger = "0.10.1" diff --git a/src/bin/wavehunter.rs b/wavehunter/src/main.rs similarity index 100% rename from src/bin/wavehunter.rs rename to wavehunter/src/main.rs From f4caac147b32699aea9f91012a95648712dd0ae7 Mon Sep 17 00:00:00 2001 From: Will Greenberg Date: Wed, 27 Dec 2023 16:13:51 -0800 Subject: [PATCH 10/16] Renaming wavehunter -> orca --- orca/tests/test_lte_parsing.rs | 4 ++-- wavehunter/src/main.rs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/orca/tests/test_lte_parsing.rs b/orca/tests/test_lte_parsing.rs index 859e5f3..48a219c 100644 --- a/orca/tests/test_lte_parsing.rs +++ b/orca/tests/test_lte_parsing.rs @@ -1,10 +1,10 @@ -use wavehunter::diag::{ +use orca::diag::{ Message, LogBody, LteRrcOtaPacket, Timestamp, }; -use wavehunter::gsmtap_parser::GsmtapParser; +use orca::gsmtap_parser::GsmtapParser; use deku::prelude::*; // Tests here are based on https://github.com/fgsect/scat/blob/97442580e628de414c9f7c2a185f4e28d0ee7523/tests/test_diagltelogparser.py diff --git a/wavehunter/src/main.rs b/wavehunter/src/main.rs index 6e106c9..f75ad04 100644 --- a/wavehunter/src/main.rs +++ b/wavehunter/src/main.rs @@ -2,10 +2,11 @@ use axum::body::Body; use axum::http::header::CONTENT_TYPE; use futures_core::Stream; use log::error; -use wavehunter::diag_device::{DiagDevice, DiagDeviceError}; -use wavehunter::diag_reader::DiagReader; -use wavehunter::gsmtap_parser::GsmtapParser; -use wavehunter::pcap::GsmtapPcapWriter; +use orca::diag_device::{DiagDevice, DiagDeviceError}; +use orca::diag_reader::DiagReader; +use orca::gsmtap_parser::GsmtapParser; +use orca::pcap::GsmtapPcapWriter; +use orca::qmdl::{QmdlReader, QmdlReaderError}; use axum::Router; use axum::extract::State; @@ -15,7 +16,6 @@ use axum::routing::get; use std::fs::File; use thiserror::Error; use serde::Deserialize; -use wavehunter::qmdl::{QmdlReader, QmdlReaderError}; use std::io::Write; use std::sync::Arc; use std::net::SocketAddr; From aa0f044b8a4b791b9f4b2faeec4f080bb4c85882 Mon Sep 17 00:00:00 2001 From: Will Greenberg Date: Wed, 27 Dec 2023 16:25:28 -0800 Subject: [PATCH 11/16] wavehunter: split out code into different modules --- wavehunter/src/config.rs | 51 +++++++++++ wavehunter/src/error.rs | 16 ++++ wavehunter/src/main.rs | 191 +++------------------------------------ wavehunter/src/server.rs | 116 ++++++++++++++++++++++++ 4 files changed, 195 insertions(+), 179 deletions(-) create mode 100644 wavehunter/src/config.rs create mode 100644 wavehunter/src/error.rs create mode 100644 wavehunter/src/server.rs diff --git a/wavehunter/src/config.rs b/wavehunter/src/config.rs new file mode 100644 index 0000000..1cd20dc --- /dev/null +++ b/wavehunter/src/config.rs @@ -0,0 +1,51 @@ +use crate::error::WavehunterError; + +use serde::Deserialize; +use toml; + +#[derive(Deserialize)] +struct ConfigFile { + qmdl_path: Option, + port: Option, +} + +#[derive(Debug)] +pub struct Config { + pub qmdl_path: String, + pub port: u16, +} + +impl Default for Config { + fn default() -> Self { + Config { + qmdl_path: "./wavehunter.qmdl".to_string(), + port: 8080, + } + } +} + +pub fn parse_config

(path: P) -> Result where P: AsRef { + let config_file = std::fs::read_to_string(&path) + .map_err(|_| WavehunterError::MissingConfigFile(format!("{:?}", path.as_ref())))?; + let parsed_config: ConfigFile = toml::from_str(&config_file) + .map_err(WavehunterError::ConfigFileParsingError)?; + let mut config = Config::default(); + parsed_config.qmdl_path.map(|path| config.qmdl_path = path); + parsed_config.port.map(|path| config.port = path); + Ok(config) +} + +pub struct Args { + pub config_path: String, +} + +pub fn parse_args() -> Args { + let args: Vec = std::env::args().collect(); + if args.len() != 2 { + println!("Usage: {} /path/to/config/file", args[0]); + std::process::exit(1); + } + Args { + config_path: args[1].clone(), + } +} diff --git a/wavehunter/src/error.rs b/wavehunter/src/error.rs new file mode 100644 index 0000000..72200a9 --- /dev/null +++ b/wavehunter/src/error.rs @@ -0,0 +1,16 @@ +use thiserror::Error; +use orca::diag_device::DiagDeviceError; + +#[derive(Error, Debug)] +pub enum WavehunterError { + #[error("Missing config file: {0}")] + MissingConfigFile(String), + #[error("Config file parsing error: {0}")] + ConfigFileParsingError(#[from] toml::de::Error), + #[error("Diag intialization error: {0}")] + DiagInitError(DiagDeviceError), + #[error("Diag read error: {0}")] + DiagReadError(DiagDeviceError), + #[error("Tokio error: {0}")] + TokioError(#[from] tokio::io::Error), +} diff --git a/wavehunter/src/main.rs b/wavehunter/src/main.rs index f75ad04..7e63924 100644 --- a/wavehunter/src/main.rs +++ b/wavehunter/src/main.rs @@ -1,90 +1,20 @@ -use axum::body::Body; -use axum::http::header::CONTENT_TYPE; -use futures_core::Stream; -use log::error; -use orca::diag_device::{DiagDevice, DiagDeviceError}; +mod config; +mod error; +mod server; + +use crate::config::{parse_config, parse_args}; +use crate::server::{ServerState, serve_pcap}; +use crate::error::WavehunterError; + +use orca::diag_device::DiagDevice; use orca::diag_reader::DiagReader; -use orca::gsmtap_parser::GsmtapParser; -use orca::pcap::GsmtapPcapWriter; -use orca::qmdl::{QmdlReader, QmdlReaderError}; -use axum::Router; -use axum::extract::State; -use axum::http::StatusCode; -use axum::response::{Response, IntoResponse}; use axum::routing::get; -use std::fs::File; -use thiserror::Error; -use serde::Deserialize; -use std::io::Write; -use std::sync::Arc; +use axum::Router; use std::net::SocketAddr; -use std::pin::Pin; -use std::task::{Poll, Context}; use tokio::net::TcpListener; -use tokio::sync::{mpsc, RwLock}; -use toml; - -#[derive(Error, Debug)] -enum WavehunterError { - #[error("Missing config file: {0}")] - MissingConfigFile(String), - #[error("Config file parsing error: {0}")] - ConfigFileParsingError(#[from] toml::de::Error), - #[error("Diag intialization error: {0}")] - DiagInitError(DiagDeviceError), - #[error("Diag read error: {0}")] - DiagReadError(DiagDeviceError), - #[error("Tokio error: {0}")] - TokioError(#[from] tokio::io::Error), -} - -#[derive(Deserialize)] -struct ConfigFile { - qmdl_path: Option, - port: Option, -} - -#[derive(Debug)] -struct Config { - qmdl_path: String, - port: u16, -} - -impl Default for Config { - fn default() -> Self { - Config { - qmdl_path: "./wavehunter.qmdl".to_string(), - port: 8080, - } - } -} - -fn parse_config

(path: P) -> Result where P: AsRef { - let config_file = std::fs::read_to_string(&path) - .map_err(|_| WavehunterError::MissingConfigFile(format!("{:?}", path.as_ref())))?; - let parsed_config: ConfigFile = toml::from_str(&config_file) - .map_err(WavehunterError::ConfigFileParsingError)?; - let mut config = Config::default(); - parsed_config.qmdl_path.map(|path| config.qmdl_path = path); - parsed_config.port.map(|path| config.port = path); - Ok(config) -} - -struct Args { - config_path: String, -} - -fn parse_args() -> Args { - let args: Vec = std::env::args().collect(); - if args.len() != 2 { - println!("Usage: {} /path/to/config/file", args[0]); - std::process::exit(1); - } - Args { - config_path: args[1].clone(), - } -} +use tokio::sync::RwLock; +use std::sync::Arc; fn run_diag_read_thread(mut dev: DiagDevice, bytes_read_lock: Arc>) -> tokio::task::JoinHandle> { tokio::task::spawn_blocking(move || { @@ -102,103 +32,6 @@ fn run_diag_read_thread(mut dev: DiagDevice, bytes_read_lock: Arc> }) } -// Streams a pcap file chunk-by-chunk to the client by reading the QMDL data -// written so far. This is done by spawning a blocking thread (a tokio thread -// capable of handling blocking operations) which streams chunks of pcap data to -// a channel that's piped to the client. -async fn serve_pcap(State(state): State>) -> Result { - let qmdl_bytes_written = *state.qmdl_bytes_written.read().await; - if qmdl_bytes_written == 0 { - return Err(( - StatusCode::SERVICE_UNAVAILABLE, - "QMDL file is empty, try again in a bit!".to_string() - )); - } - - let (tx, rx) = mpsc::channel(1); - let channel_reader = ChannelReader { rx }; - let channel_writer = ChannelWriter { tx }; - tokio::task::spawn_blocking(move || { - // the QMDL reader should stop at the last successfully written data - // chunk (qmdl_bytes_written) - let qmdl_file = File::open(&state.qmdl_path).unwrap(); - let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(qmdl_bytes_written)); - - let mut gsmtap_parser = GsmtapParser::new(); - let mut pcap_writer = GsmtapPcapWriter::new(channel_writer).unwrap(); - pcap_writer.write_iface_header().unwrap(); - loop { - match qmdl_reader.read_response() { - Ok(messages) => { - for maybe_msg in messages { - match maybe_msg { - Ok(msg) => { - let maybe_gsmtap_msg = gsmtap_parser.recv_message(msg) - .expect("error parsing gsmtap message"); - if let Some((timestamp, gsmtap_msg)) = maybe_gsmtap_msg { - pcap_writer.write_gsmtap_message(gsmtap_msg, timestamp) - .expect("error writing pcap packet"); - } - }, - Err(e) => { - error!("error parsing message: {:?}", e); - }, - } - } - }, - // this is expected, and just means we've reached the end of the - // safely written QMDL data - Err(QmdlReaderError::MaxBytesReached(_)) => break, - Err(e) => { - error!("error reading qmdl file: {:?}", e); - break; - }, - } - } - }); - - let headers = [(CONTENT_TYPE, "application/vnd.tcpdump.pcap")]; - let body = Body::from_stream(channel_reader); - Ok((headers, body).into_response()) -} - -struct ChannelWriter { - tx: mpsc::Sender>, -} - -impl Write for ChannelWriter { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - self.tx.blocking_send(buf.to_vec()) - .map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, "channel closed"))?; - Ok(buf.len()) - } - - fn flush(&mut self) -> std::io::Result<()> { - Ok(()) - } -} - -struct ChannelReader { - rx: mpsc::Receiver>, -} - -impl Stream for ChannelReader { - type Item = Result, String>; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match self.rx.poll_recv(cx) { - Poll::Ready(Some(msg)) => Poll::Ready(Some(Ok(msg))), - Poll::Ready(None) => Poll::Ready(None), - Poll::Pending => Poll::Pending, - } - } -} - -struct ServerState { - qmdl_bytes_written: Arc>, - qmdl_path: String, -} - #[tokio::main] async fn main() -> Result<(), WavehunterError> { env_logger::init(); diff --git a/wavehunter/src/server.rs b/wavehunter/src/server.rs new file mode 100644 index 0000000..95b74e9 --- /dev/null +++ b/wavehunter/src/server.rs @@ -0,0 +1,116 @@ +use orca::gsmtap_parser::GsmtapParser; +use orca::pcap::GsmtapPcapWriter; +use orca::qmdl::{QmdlReader, QmdlReaderError}; +use orca::diag_reader::DiagReader; + +use axum::body::Body; +use axum::http::header::CONTENT_TYPE; +use axum::extract::State; +use axum::http::StatusCode; +use axum::response::{Response, IntoResponse}; +use std::fs::File; +use std::io::Write; +use std::pin::Pin; +use std::sync::Arc; +use tokio::sync::RwLock; +use std::task::{Poll, Context}; +use futures_core::Stream; +use log::error; +use tokio::sync::mpsc; + +// Streams a pcap file chunk-by-chunk to the client by reading the QMDL data +// written so far. This is done by spawning a blocking thread (a tokio thread +// capable of handling blocking operations) which streams chunks of pcap data to +// a channel that's piped to the client. +pub async fn serve_pcap(State(state): State>) -> Result { + let qmdl_bytes_written = *state.qmdl_bytes_written.read().await; + if qmdl_bytes_written == 0 { + return Err(( + StatusCode::SERVICE_UNAVAILABLE, + "QMDL file is empty, try again in a bit!".to_string() + )); + } + + let (tx, rx) = mpsc::channel(1); + let channel_reader = ChannelReader { rx }; + let channel_writer = ChannelWriter { tx }; + tokio::task::spawn_blocking(move || { + // the QMDL reader should stop at the last successfully written data + // chunk (qmdl_bytes_written) + let qmdl_file = File::open(&state.qmdl_path).unwrap(); + let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(qmdl_bytes_written)); + + let mut gsmtap_parser = GsmtapParser::new(); + let mut pcap_writer = GsmtapPcapWriter::new(channel_writer).unwrap(); + pcap_writer.write_iface_header().unwrap(); + loop { + match qmdl_reader.read_response() { + Ok(messages) => { + for maybe_msg in messages { + match maybe_msg { + Ok(msg) => { + let maybe_gsmtap_msg = gsmtap_parser.recv_message(msg) + .expect("error parsing gsmtap message"); + if let Some((timestamp, gsmtap_msg)) = maybe_gsmtap_msg { + pcap_writer.write_gsmtap_message(gsmtap_msg, timestamp) + .expect("error writing pcap packet"); + } + }, + Err(e) => { + error!("error parsing message: {:?}", e); + }, + } + } + }, + // this is expected, and just means we've reached the end of the + // safely written QMDL data + Err(QmdlReaderError::MaxBytesReached(_)) => break, + Err(e) => { + error!("error reading qmdl file: {:?}", e); + break; + }, + } + } + }); + + let headers = [(CONTENT_TYPE, "application/vnd.tcpdump.pcap")]; + let body = Body::from_stream(channel_reader); + Ok((headers, body).into_response()) +} + +struct ChannelWriter { + tx: mpsc::Sender>, +} + +impl Write for ChannelWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.tx.blocking_send(buf.to_vec()) + .map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, "channel closed"))?; + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +struct ChannelReader { + rx: mpsc::Receiver>, +} + +impl Stream for ChannelReader { + type Item = Result, String>; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.rx.poll_recv(cx) { + Poll::Ready(Some(msg)) => Poll::Ready(Some(Ok(msg))), + Poll::Ready(None) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + } + } +} + +pub struct ServerState { + pub qmdl_bytes_written: Arc>, + pub qmdl_path: String, +} From 4d80f992acad856fda8d3df6c39fea64f766c06e Mon Sep 17 00:00:00 2001 From: Will Greenberg Date: Thu, 28 Dec 2023 12:59:34 -0800 Subject: [PATCH 12/16] pcap: fix timestamp values --- orca/src/pcap.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/orca/src/pcap.rs b/orca/src/pcap.rs index 2f90bd3..9ae59ca 100644 --- a/orca/src/pcap.rs +++ b/orca/src/pcap.rs @@ -17,6 +17,8 @@ pub enum GsmtapPcapError { Io(#[from] std::io::Error), #[error("Pcap error: {0}")] Pcap(#[from] PcapError), + #[error("Timestamp out of range: {0}")] + TimestampOutOfRange(#[from] chrono::OutOfRangeError), #[error("Deku error: {0}")] Deku(#[from] DekuError), } @@ -71,12 +73,15 @@ impl GsmtapPcapWriter where T: Write { } pub fn write_gsmtap_message(&mut self, msg: GsmtapMessage, timestamp: Timestamp) -> Result<(), GsmtapPcapError> { - let time_since_epoch = timestamp.to_datetime().signed_duration_since(DateTime::UNIX_EPOCH); - let secs_since_epoch = time_since_epoch.num_seconds() as u64; - let nsecs_since_epoch = time_since_epoch.num_nanoseconds().unwrap_or(0) as u32; - // FIXME: although the duration value is correct here, when it shows up in - // the pcap it's WAY off, like in the year 55920 - let duration = std::time::Duration::new(secs_since_epoch, nsecs_since_epoch); + let duration = timestamp.to_datetime() + .signed_duration_since(DateTime::UNIX_EPOCH) + .to_std()?; + + // despite the timestamp above being correct, we have reduce it by + // orders of magnitude due to a bug in pcap_file: + // https://github.com/courvoif/pcap-file/pull/32 + let duration = std::time::Duration::from_nanos(duration.as_micros() as u64); + let msg_bytes = msg.to_bytes()?; let ip_header = IpHeader { version_and_ihl: 0x45, From 65b41bd54167831c44d632a6bacfb20d44d1db07 Mon Sep 17 00:00:00 2001 From: Will Greenberg Date: Thu, 28 Dec 2023 12:59:47 -0800 Subject: [PATCH 13/16] wavehunter: add debug mode This just runs the server off of a QMDL file, so we can run/test the server locally w/o the Qualcomm hardware --- wavehunter/src/config.rs | 6 ++++- wavehunter/src/main.rs | 53 ++++++++++++++++++++++++---------------- 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/wavehunter/src/config.rs b/wavehunter/src/config.rs index 1cd20dc..2b081a6 100644 --- a/wavehunter/src/config.rs +++ b/wavehunter/src/config.rs @@ -7,12 +7,14 @@ use toml; struct ConfigFile { qmdl_path: Option, port: Option, + debug_mode: Option, } #[derive(Debug)] pub struct Config { pub qmdl_path: String, pub port: u16, + pub debug_mode: bool, } impl Default for Config { @@ -20,6 +22,7 @@ impl Default for Config { Config { qmdl_path: "./wavehunter.qmdl".to_string(), port: 8080, + debug_mode: false, } } } @@ -31,7 +34,8 @@ pub fn parse_config

(path: P) -> Result where P: AsRe .map_err(WavehunterError::ConfigFileParsingError)?; let mut config = Config::default(); parsed_config.qmdl_path.map(|path| config.qmdl_path = path); - parsed_config.port.map(|path| config.port = path); + parsed_config.port.map(|port| config.port = port); + parsed_config.debug_mode.map(|debug_mode| config.debug_mode = debug_mode); Ok(config) } diff --git a/wavehunter/src/main.rs b/wavehunter/src/main.rs index 7e63924..ca741bb 100644 --- a/wavehunter/src/main.rs +++ b/wavehunter/src/main.rs @@ -11,6 +11,7 @@ use orca::diag_reader::DiagReader; use axum::routing::get; use axum::Router; +use tokio::fs::File; use std::net::SocketAddr; use tokio::net::TcpListener; use tokio::sync::RwLock; @@ -32,6 +33,21 @@ fn run_diag_read_thread(mut dev: DiagDevice, bytes_read_lock: Arc> }) } +async fn run_server(config: &config::Config, qmdl_bytes_written: Arc>) -> Result<(), WavehunterError> { + let state = Arc::new(ServerState { + qmdl_bytes_written, + qmdl_path: config.qmdl_path.clone(), + }); + + let app = Router::new() + .route("/output.pcap", get(serve_pcap)) + .with_state(state); + let addr = SocketAddr::from(([127, 0, 0, 1], config.port)); + let listener = TcpListener::bind(&addr).await.unwrap(); + axum::serve(listener, app).await.unwrap(); + Ok(()) +} + #[tokio::main] async fn main() -> Result<(), WavehunterError> { env_logger::init(); @@ -39,28 +55,23 @@ async fn main() -> Result<(), WavehunterError> { let args = parse_args(); let config = parse_config(&args.config_path)?; - let mut dev = DiagDevice::new(&config.qmdl_path) - .map_err(WavehunterError::DiagInitError)?; - dev.config_logs() - .map_err(WavehunterError::DiagInitError)?; + let qmdl_bytes_lock: Arc>; + if !config.debug_mode { + let mut dev = DiagDevice::new(&config.qmdl_path) + .map_err(WavehunterError::DiagInitError)?; + dev.config_logs() + .map_err(WavehunterError::DiagInitError)?; + qmdl_bytes_lock = Arc::new(RwLock::new(dev.qmdl_writer.total_written)); - let qmdl_bytes_lock = Arc::new(RwLock::new(dev.qmdl_writer.total_written)); - // TODO: handle exiting gracefully - let _read_thread_handle = run_diag_read_thread(dev, qmdl_bytes_lock.clone()); + // TODO: handle exiting gracefully + let _read_thread_handle = run_diag_read_thread(dev, qmdl_bytes_lock.clone()); + } else { + let qmdl_file = File::open(&config.qmdl_path).await.expect("couldn't open QMDL file"); + let qmdl_file_size = qmdl_file.metadata().await.expect("couldn't get QMDL file metadata") + .len() as usize; + qmdl_bytes_lock = Arc::new(RwLock::new(qmdl_file_size)); + } println!("The orca is hunting for stingrays..."); - - let addr = SocketAddr::from(([127, 0, 0, 1], config.port)); - let listener = TcpListener::bind(&addr).await?; - let state = Arc::new(ServerState { - qmdl_bytes_written: qmdl_bytes_lock, - qmdl_path: config.qmdl_path, - }); - - let app = Router::new() - .route("/output.pcap", get(serve_pcap)) - .with_state(state); - axum::serve(listener, app).await.unwrap(); - - Ok(()) + run_server(&config, qmdl_bytes_lock).await } From 13e0ed2679901ca8e4a9741c7a376fcb18f08b37 Mon Sep 17 00:00:00 2001 From: Will Greenberg Date: Tue, 2 Jan 2024 11:33:36 -0800 Subject: [PATCH 14/16] Appease clippy --- orca/src/diag_device.rs | 18 +++++++++++++----- orca/src/diag_reader.rs | 4 ++-- orca/src/gsmtap_parser.rs | 8 +++++++- orca/src/hdlc.rs | 2 +- wavehunter/src/config.rs | 7 +++---- 5 files changed, 26 insertions(+), 13 deletions(-) diff --git a/orca/src/diag_device.rs b/orca/src/diag_device.rs index 574c083..1b1d471 100644 --- a/orca/src/diag_device.rs +++ b/orca/src/diag_device.rs @@ -58,8 +58,16 @@ pub const LOG_CODES_FOR_RAW_PACKET_LOGGING: [u32; 11] = [ const BUFFER_LEN: usize = 1024 * 1024 * 10; const MEMORY_DEVICE_MODE: i32 = 2; + +#[cfg(target_arch = "arm")] const DIAG_IOCTL_REMOTE_DEV: u32 = 32; +#[cfg(target_arch = "x86_64")] +const DIAG_IOCTL_REMOTE_DEV: u64 = 32; + +#[cfg(target_arch = "arm")] const DIAG_IOCTL_SWITCH_LOGGING: u32 = 7; +#[cfg(target_arch = "x86_64")] +const DIAG_IOCTL_SWITCH_LOGGING: u64 = 7; pub struct DiagDevice { file: File, @@ -80,7 +88,7 @@ impl DiagReader for DiagDevice { } let ((leftover_bytes, _), container) = MessagesContainer::from_bytes((&self.read_buf[0..bytes_read], 0)) .map_err(DiagDeviceError::ParseMessagesContainerError)?; - if leftover_bytes.len() > 0 { + if !leftover_bytes.is_empty() { warn!("warning: {} leftover bytes when parsing MessagesContainer", leftover_bytes.len()); } @@ -134,7 +142,7 @@ impl DiagDevice { data_type: DataType::UserSpace, use_mdm: self.use_mdm > 0, mdm_field: -1, - hdlc_encapsulated_request: hdlc_encapsulate(&req_bytes, &CRC_CCITT), + hdlc_encapsulated_request: hdlc_encapsulate(req_bytes, &CRC_CCITT), }.to_bytes().expect("Failed to serialize RequestContainer"); unsafe { let fd = self.file.as_raw_fd(); @@ -212,10 +220,10 @@ impl DiagDevice { // Triggers the diag device's debug logging mode fn enable_frame_readwrite(fd: i32, mode: i32) -> DiagResult<()> { unsafe { - if libc::ioctl(fd, DIAG_IOCTL_SWITCH_LOGGING.into(), mode, 0, 0, 0) < 0 { + if libc::ioctl(fd, DIAG_IOCTL_SWITCH_LOGGING, mode, 0, 0, 0) < 0 { let ret = libc::ioctl( fd, - DIAG_IOCTL_SWITCH_LOGGING.into(), + DIAG_IOCTL_SWITCH_LOGGING, &mut [mode, -1, 0] as *mut _, // diag_logging_mode_param_t std::mem::size_of::<[i32; 3]>(), 0, 0, 0, 0 ); @@ -233,7 +241,7 @@ fn enable_frame_readwrite(fd: i32, mode: i32) -> DiagResult<()> { fn determine_use_mdm(fd: i32) -> DiagResult { let use_mdm: i32 = 0; unsafe { - if libc::ioctl(fd, DIAG_IOCTL_REMOTE_DEV.into(), &use_mdm as *const i32) < 0 { + if libc::ioctl(fd, DIAG_IOCTL_REMOTE_DEV, &use_mdm as *const i32) < 0 { let msg = format!("DIAG_IOCTL_REMOTE_DEV ioctl failed with error code {}", 0); return Err(DiagDeviceError::InitializationFailed(msg)) } diff --git a/orca/src/diag_reader.rs b/orca/src/diag_reader.rs index c0f1678..b8ef308 100644 --- a/orca/src/diag_reader.rs +++ b/orca/src/diag_reader.rs @@ -51,10 +51,10 @@ pub trait DiagReader { let mut result = Vec::new(); for msg in container.messages { for sub_msg in msg.data.split_inclusive(|&b| b == diag::MESSAGE_TERMINATOR) { - match hdlc_decapsulate(&sub_msg, &CRC_CCITT) { + match hdlc_decapsulate(sub_msg, &CRC_CCITT) { Ok(data) => match Message::from_bytes((&data, 0)) { Ok(((leftover_bytes, _), res)) => { - if leftover_bytes.len() > 0 { + if !leftover_bytes.is_empty() { warn!("warning: {} leftover bytes when parsing Message", leftover_bytes.len()); } result.push(Ok(res)); diff --git a/orca/src/gsmtap_parser.rs b/orca/src/gsmtap_parser.rs index 540e300..8ca9cfb 100644 --- a/orca/src/gsmtap_parser.rs +++ b/orca/src/gsmtap_parser.rs @@ -7,6 +7,12 @@ use thiserror::Error; pub struct GsmtapParser { } +impl Default for GsmtapParser { + fn default() -> Self { + GsmtapParser::new() + } +} + #[derive(Debug, Error)] pub enum GsmtapParserError { #[error("Invalid LteRrcOtaMessage ext header version {0}")] @@ -57,7 +63,7 @@ impl GsmtapParser { 15 => GsmtapType::LteRrc(LteRrcSubtype::UlDcch), pdu => return Err(GsmtapParserError::InvalidLteRrcOtaHeaderPduNum(ext_header_version, pdu)), }, - 0x0e | 0x0f | 0x10 => match packet.get_pdu_num() { + 0x0e..=0x10 => match packet.get_pdu_num() { 1 => GsmtapType::LteRrc(LteRrcSubtype::BcchBch), 2 => GsmtapType::LteRrc(LteRrcSubtype::BcchDlSch), 4 => GsmtapType::LteRrc(LteRrcSubtype::MCCH), diff --git a/orca/src/hdlc.rs b/orca/src/hdlc.rs index 9e0ba0f..3c99e1b 100644 --- a/orca/src/hdlc.rs +++ b/orca/src/hdlc.rs @@ -34,7 +34,7 @@ pub fn hdlc_encapsulate(data: &[u8], crc: &Crc) -> Vec { } } - for b in crc.checksum(&data).to_le_bytes() { + for b in crc.checksum(data).to_le_bytes() { match b { MESSAGE_TERMINATOR => result.extend([MESSAGE_ESCAPE_CHAR, ESCAPED_MESSAGE_TERMINATOR]), MESSAGE_ESCAPE_CHAR => result.extend([MESSAGE_ESCAPE_CHAR, ESCAPED_MESSAGE_ESCAPE_CHAR]), diff --git a/wavehunter/src/config.rs b/wavehunter/src/config.rs index 2b081a6..d8280f8 100644 --- a/wavehunter/src/config.rs +++ b/wavehunter/src/config.rs @@ -1,7 +1,6 @@ use crate::error::WavehunterError; use serde::Deserialize; -use toml; #[derive(Deserialize)] struct ConfigFile { @@ -33,9 +32,9 @@ pub fn parse_config

(path: P) -> Result where P: AsRe let parsed_config: ConfigFile = toml::from_str(&config_file) .map_err(WavehunterError::ConfigFileParsingError)?; let mut config = Config::default(); - parsed_config.qmdl_path.map(|path| config.qmdl_path = path); - parsed_config.port.map(|port| config.port = port); - parsed_config.debug_mode.map(|debug_mode| config.debug_mode = debug_mode); + if let Some(path) = parsed_config.qmdl_path { config.qmdl_path = path } + if let Some(port) = parsed_config.port { config.port = port } + if let Some(debug_mode) = parsed_config.debug_mode { config.debug_mode = debug_mode } Ok(config) } From 84f2fc17510a979f4efb6d8312fe985bd1c645d8 Mon Sep 17 00:00:00 2001 From: Will Greenberg Date: Tue, 2 Jan 2024 11:33:53 -0800 Subject: [PATCH 15/16] wavehunter: add some debug logging for blocking calls This may come in handy if the read/write loop ever gets stuck. --- wavehunter/src/main.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/wavehunter/src/main.rs b/wavehunter/src/main.rs index ca741bb..f9cd13f 100644 --- a/wavehunter/src/main.rs +++ b/wavehunter/src/main.rs @@ -6,6 +6,7 @@ use crate::config::{parse_config, parse_args}; use crate::server::{ServerState, serve_pcap}; use crate::error::WavehunterError; +use log::debug; use orca::diag_device::DiagDevice; use orca::diag_reader::DiagReader; @@ -23,12 +24,16 @@ fn run_diag_read_thread(mut dev: DiagDevice, bytes_read_lock: Arc> // TODO: once we're actually doing analysis, we'll wanna use the messages // returned here. Until then, the DiagDevice has already written those messages // to the QMDL file, so we can just ignore them. + debug!("reading response from diag device..."); let _messages = dev.read_response().map_err(WavehunterError::DiagReadError)?; + debug!("got diag response ({} messages)", _messages.len()); // keep track of how many bytes were written to the QMDL file so we can read // a valid block of data from it in the HTTP server + debug!("total QMDL bytes written: {}, updating state...", dev.qmdl_writer.total_written); let mut bytes_read = bytes_read_lock.blocking_write(); *bytes_read = dev.qmdl_writer.total_written; + debug!("done!"); } }) } From 7037928a6ec29d685b4ffaad09d80b1cc2336064 Mon Sep 17 00:00:00 2001 From: Will Greenberg Date: Tue, 2 Jan 2024 12:50:48 -0800 Subject: [PATCH 16/16] wavehunter: add route to serve qmdl file This is easier than pulling files via adb --- Cargo.lock | 1 + wavehunter/Cargo.toml | 1 + wavehunter/src/main.rs | 3 ++- wavehunter/src/server.rs | 19 ++++++++++++++++--- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 80765a9..d29868d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1253,6 +1253,7 @@ dependencies = [ "serde", "thiserror", "tokio", + "tokio-util", "toml", ] diff --git a/wavehunter/Cargo.toml b/wavehunter/Cargo.toml index 914a8df..067e353 100644 --- a/wavehunter/Cargo.toml +++ b/wavehunter/Cargo.toml @@ -15,3 +15,4 @@ futures-core = "0.3.30" thiserror = "1.0.52" log = "0.4.20" env_logger = "0.10.1" +tokio-util = { version = "0.7.10", features = ["io"] } diff --git a/wavehunter/src/main.rs b/wavehunter/src/main.rs index f9cd13f..0b74fa8 100644 --- a/wavehunter/src/main.rs +++ b/wavehunter/src/main.rs @@ -3,7 +3,7 @@ mod error; mod server; use crate::config::{parse_config, parse_args}; -use crate::server::{ServerState, serve_pcap}; +use crate::server::{ServerState, serve_pcap, serve_qmdl}; use crate::error::WavehunterError; use log::debug; @@ -46,6 +46,7 @@ async fn run_server(config: &config::Config, qmdl_bytes_written: Arc>) -> Result { - error!("error parsing message: {:?}", e); - }, + Err(e) => error!("error parsing message: {:?}", e), } } }, @@ -110,6 +111,18 @@ impl Stream for ChannelReader { } } +pub async fn serve_qmdl(State(state): State>) -> Result { + let qmdl_file = AsyncFile::open(&state.qmdl_path).await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("error opening QMDL file: {}", e)))?; + let qmdl_bytes_written = *state.qmdl_bytes_written.read().await; + let limited_qmdl_file = qmdl_file.take(qmdl_bytes_written as u64); + let qmdl_stream = ReaderStream::new(limited_qmdl_file); + + let headers = [(CONTENT_TYPE, "application/octet-stream")]; + let body = Body::from_stream(qmdl_stream); + Ok((headers, body).into_response()) +} + pub struct ServerState { pub qmdl_bytes_written: Arc>, pub qmdl_path: String,