diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 0000000..6e64999 --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,14 @@ +on: [pull_request] +name: benchmark pull requests +jobs: + runBenchmark: + name: run benchmark + runs-on: ubuntu-latest + steps: + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libpcap-dev + - uses: actions/checkout@v3 + - uses: boa-dev/criterion-compare-action@v3 + with: + features: "resolve" + branchName: ${{ github.base_ref }} diff --git a/Cargo.lock b/Cargo.lock index b41ef2e..e35eda4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -364,12 +364,12 @@ checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libloading" -version = "0.6.7" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "351a32417a12d5f7e82c368a66781e307834dae04c6ce0cd4456d52989229883" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "winapi", + "windows-targets 0.52.0", ] [[package]] @@ -407,9 +407,9 @@ checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" [[package]] name = "pcap" -version = "1.2.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77452fdf9d211d9ca35d092aeefe4d4b3f0c4eb529ffb87a8a3b8fe2bb7c37c3" +checksum = "fe4d339439e5e7f8ce32d58c2b58d5e304790e66f3aa0bd391dd6a9dc676e054" dependencies = [ "bitflags", "errno", @@ -428,7 +428,7 @@ checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "pktstrings" -version = "1.4.1" +version = "1.5.0" dependencies = [ "cfg-if", "clap", diff --git a/Cargo.toml b/Cargo.toml index 488e669..87fa75c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pktstrings" -version = "1.4.1" +version = "1.5.0" edition = "2021" rust-version = "1.60" authors = ["Pete Wicken "] @@ -13,10 +13,10 @@ keywords = ["packet-analyzer", "pcap", "sniffing", "packet"] categories = ["command-line-utilities", "network-programming"] [dependencies] -clap = {version = "4", features = ["derive"]} -pcap = "1" +clap = { version = "4", features = ["derive"] } +pcap = "2" colored = "2" -dns-lookup = {version = "2", optional = true} +dns-lookup = { version = "2", optional = true } cfg-if = "1" libc = "0" regex = "1" @@ -51,3 +51,11 @@ harness = false [[bench]] name = "strings" harness = false + +[[bench]] +name = "getfield_func_vs_macro" +harness = false + +[[bench]] +name = "buffer_strings_vs_print_strings" +harness = false diff --git a/benches/buffer_strings_vs_print_strings.rs b/benches/buffer_strings_vs_print_strings.rs new file mode 100644 index 0000000..14d5901 --- /dev/null +++ b/benches/buffer_strings_vs_print_strings.rs @@ -0,0 +1,160 @@ +use colored::Colorize; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use pcap::{Activated, Capture}; +use std::path::Path; + +use pktstrings::net; + +const PCAP: &str = "./benches/data/http.pcap"; + +pub fn print_strings( + cap: &mut Capture, + len: &usize, + resolver: &mut Option>, + block_print: &bool, +) { + let mut pkt_count = 0; + + while let Ok(pkt) = cap.next_packet() { + pkt_count += 1; + + let mut printed = false; + let mut chars = 0; + let mut partial = String::new(); + let mut pkt_str: Option = None; + for byte in pkt.data { + let c = *byte as char; + // TODO: other encodings + if c.is_ascii() && !c.is_ascii_control() { + chars += 1; + if chars > *len { + print!("{}", c); + } else { + partial.push(c); + if chars == *len { + if pkt_str.is_none() { + if let Some(ref mut r) = resolver { + let mut pktsum = net::PacketSummary::from_packet(&pkt, Some(r)); + pkt_str = Some(pktsum.formatted()); + } else { + let mut pktsum = net::PacketSummary::from_packet(&pkt, None); + pkt_str = Some(pktsum.formatted()); + } + } + + let idx = pkt_count.to_string().blue(); + if !printed || !*block_print { + if let Some(ref mut pkt_str) = pkt_str { + print!("[{idx}]{pkt_str}: "); + printed = true; + if *block_print { + println!(); + } + } + } + print!("{partial}"); + } + } + } else { + if chars >= *len { + println!(); + } + chars = 0; + partial.clear(); + } + } + if chars >= *len { + println!(); + } + } +} + +pub fn buffer_strings( + cap: &mut Capture, + len: &usize, + resolver: &mut Option>, + block_print: &bool, +) { + let mut pkt_count = 0; + + while let Ok(pkt) = cap.next_packet() { + pkt_count += 1; + + let mut found = false; + let mut chars = 0; + let mut display_string = String::new(); + let mut partial = String::new(); + let mut pkt_str: Option = None; + for byte in pkt.data { + let c = *byte as char; + // TODO: other encodings + if c.is_ascii() && !c.is_ascii_control() { + chars += 1; + if chars > *len { + display_string.push(c); + } else { + partial.push(c); + if chars == *len { + if pkt_str.is_none() { + if let Some(ref mut r) = resolver { + let mut pktsum = net::PacketSummary::from_packet(&pkt, Some(r)); + pkt_str = Some(pktsum.formatted()); + } else { + let mut pktsum = net::PacketSummary::from_packet(&pkt, None); + pkt_str = Some(pktsum.formatted()); + } + } + + let idx = pkt_count.to_string().blue(); + if !found || !*block_print { + if let Some(ref mut pkt_str) = pkt_str { + display_string.push_str(format!("[{idx}]{pkt_str}: ").as_str()); + found = true; + if *block_print { + display_string.push('\n'); + } + } + } + display_string.push_str(partial.as_str()); + partial.clear(); + } + } + } else { + // print when we encounter non-ascii + if chars >= *len { + println!("{}", display_string); + } else { + partial.clear(); + display_string.clear() + } + chars = 0; + } + } + // print if we hit end of packet but havent dumped buffer yet + if chars >= *len { + println!("{}", display_string); + } + } +} + +fn dump_strings_benches(c: &mut Criterion) { + let mut pktstring_group = Criterion::benchmark_group(c, "String Dump Comparisons"); + pktstring_group.bench_function(BenchmarkId::new("print_strings", "http_pcap"), |b| { + let filepath = Path::new(PCAP); + let mut cap = Capture::from_file(filepath).unwrap(); + b.iter(|| { + print_strings(&mut cap, &7, &mut None, &false); + }); + }); + pktstring_group.bench_function(BenchmarkId::new("buffer_strings", "http_pcap"), |b| { + let filepath = Path::new(PCAP); + let mut cap = Capture::from_file(filepath).unwrap(); + b.iter(|| { + buffer_strings(&mut cap, &7, &mut None, &false); + }); + }); + pktstring_group.finish(); +} + +criterion_group!(benches, dump_strings_benches); +criterion_main!(benches); diff --git a/benches/data/http.pcap b/benches/data/http.pcap new file mode 100644 index 0000000..072609f Binary files /dev/null and b/benches/data/http.pcap differ diff --git a/benches/getfield_func_vs_macro.rs b/benches/getfield_func_vs_macro.rs new file mode 100644 index 0000000..4b7d673 --- /dev/null +++ b/benches/getfield_func_vs_macro.rs @@ -0,0 +1,107 @@ +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; + +mod meta; + +pub fn get_field(data: &[u8], offset: usize, bytelen: usize) -> Result { + assert!(bytelen <= 16, "Length must be less than 16 bytes"); + if (data.len() - offset) < bytelen { + return Err("Data after offset is shorter than bytelen"); + } + let mut addr: u128 = 0; + for i in 0..(bytelen) { + addr |= (data[offset + i] as u128) << (((bytelen - 1) * 8) - (i * 8)) + } + Ok(addr) +} + +macro_rules! m_get_field { + ($data:expr, $offset:expr, $bits:expr, $uint_type:ty) => {{ + let uint_sz = std::mem::size_of::<$uint_type>(); + let bytes = ($bits / 8) as $uint_type; + let bytes_round = if bytes > 1 { bytes } else { 1 }; + assert!( + ($bits / 8) <= uint_sz, + "Attempt to read more bits than possible in type" + ); + if ($data.len() - $offset) < ($bits / 8) as usize { + Err("Data after offset is shorter than uint_type") + } else { + let mut val: $uint_type = 0; + for i in 0..(bytes_round) { + let idx = ($offset as $uint_type + i) as usize; + let data_byte = $data[idx] as $uint_type; + val |= data_byte << ((bytes_round - 1) * 8) - (i * 8); + } + val &= ((1u128 << $bits as u128) - 1) as $uint_type; + Ok(val) + } + }}; +} + +fn get_field_benches(c: &mut Criterion) { + let mut field_group = c.benchmark_group("Byte Field Conversion Comparisons"); + for bytelen in 0..16 { + field_group.throughput(Throughput::Bytes(bytelen as u64)); + + field_group.bench_with_input( + BenchmarkId::new("func_field_select", bytelen), + &bytelen, + |b, bytelen| { + b.iter(|| get_field(meta::DATA, 0, *bytelen)); + }, + ); + + match bytelen { + 0..=1 => { + field_group.bench_with_input( + BenchmarkId::new("macro_field_select", bytelen), + &bytelen, + |b, bytelen| { + b.iter(|| m_get_field!(meta::DATA, 0, *bytelen, u8)); + }, + ); + } + 2 => { + field_group.bench_with_input( + BenchmarkId::new("macro_field_select", bytelen), + &bytelen, + |b, bytelen| { + b.iter(|| m_get_field!(meta::DATA, 0, *bytelen, u16)); + }, + ); + } + 3..=4 => { + field_group.bench_with_input( + BenchmarkId::new("macro_field_select", bytelen), + &bytelen, + |b, bytelen| { + b.iter(|| m_get_field!(meta::DATA, 0, *bytelen, u32)); + }, + ); + } + 5..=8 => { + field_group.bench_with_input( + BenchmarkId::new("macro_field_select", bytelen), + &bytelen, + |b, bytelen| { + b.iter(|| m_get_field!(meta::DATA, 0, *bytelen, u64)); + }, + ); + } + 9..=16 => { + field_group.bench_with_input( + BenchmarkId::new("macro_field_select", bytelen), + &bytelen, + |b, bytelen| { + b.iter(|| m_get_field!(meta::DATA, 0, *bytelen, u128)); + }, + ); + } + _ => {} + } + } + field_group.finish(); +} + +criterion_group!(benches, get_field_benches); +criterion_main!(benches); diff --git a/benches/net.rs b/benches/net.rs index 984ed1c..06e6e3b 100644 --- a/benches/net.rs +++ b/benches/net.rs @@ -12,7 +12,7 @@ fn net(c: &mut Criterion) { BenchmarkId::new("get_field", bytelen), &bytelen, |b, bytelen| { - b.iter(|| net::get_field(meta::DATA, 0, *bytelen)); + b.iter(|| net::get_field!(meta::DATA, 0, *bytelen, u128)); }, ); } diff --git a/src/bin/pktstrings.rs b/src/bin/pktstrings.rs index fd4c8c3..4eab800 100644 --- a/src/bin/pktstrings.rs +++ b/src/bin/pktstrings.rs @@ -60,7 +60,7 @@ struct Cli { interface: Option, } -/// Applys the provided BPF filter to the capture file. +/// Applies the provided BPF filter to the capture file. fn apply_filter(cap: &mut Capture, bpf: &Option, cmd: &mut clap::Command) { if let Some(bpf) = bpf { match cap.filter(bpf, true) { @@ -72,18 +72,15 @@ fn apply_filter(cap: &mut Capture, bpf: &Option, cmd: & /// Print to stdout the available network devices. fn list_devices() { - Device::list() - .unwrap_or_default() - .iter() - .for_each(|dev| { - let mut dev = dev.name.to_string().normal(); - if let Some(default_dev) = Device::lookup().ok().flatten() { - if *dev == *default_dev.name { - dev = dev.bold(); - } + Device::list().unwrap_or_default().iter().for_each(|dev| { + let mut dev = dev.name.to_string().normal(); + if let Some(default_dev) = Device::lookup().ok().flatten() { + if *dev == *default_dev.name { + dev = dev.bold(); } - print!("{dev}\t"); - }); + } + print!("{dev}\t"); + }); println!(); } diff --git a/src/lib/net.rs b/src/lib/net.rs index fe6bf88..0c626ae 100644 --- a/src/lib/net.rs +++ b/src/lib/net.rs @@ -12,12 +12,36 @@ use crate::proto::*; #[cfg(feature = "resolve")] use dns_lookup::lookup_addr; +#[macro_export] +macro_rules! get_field { + ($data:expr, $offset:expr, $bytelen:expr, $uint_type:ty) => {{ + let uint_sz = std::mem::size_of::<$uint_type>(); + assert!( + $bytelen <= uint_sz, + "Attempt to read more bytes than possible in type" + ); + if ($data.len() - $offset) < $bytelen as usize { + Err("Data after offset is shorter than bytelen") + } else { + let mut val: $uint_type = 0; + for i in 0..($bytelen) { + let idx = $offset + i; + let data_byte = $data[idx] as $uint_type; + val |= data_byte << ((($bytelen - 1) * 8) - (i * 8)) + } + Ok(val) + } + }}; +} + +pub use get_field; + pub type Resolver = HashMap; #[derive(Eq, PartialEq, Default)] pub struct PacketSummary<'a> { - pub l2_src: Option, - pub l2_dst: Option, + pub l2_src: Option, + pub l2_dst: Option, pub ethertype: Option, pub vlan_id: Option, pub l3_src: Option, @@ -64,18 +88,6 @@ pub fn handle_protocol( handler(pkt, offset, pktsum) } -pub fn get_field(data: &[u8], offset: usize, bytelen: usize) -> Result { - assert!(bytelen <= 16, "Length must be less than 16 bytes"); - if (data.len() - offset) < bytelen { - return Err("Data after offset is shorter than bytelen"); - } - let mut addr: u128 = 0; - for i in 0..(bytelen) { - addr |= (data[offset + i] as u128) << (((bytelen - 1) * 8) - (i * 8)) - } - Ok(addr) -} - pub fn int_to_mac_str(addr: &u64, formatted: &mut String) { let bytes = addr.to_be_bytes(); write!(formatted, "{:02x}", bytes[2]).unwrap(); @@ -129,9 +141,9 @@ pub fn handle_eth( offset: usize, pktsum: &mut PacketSummary, ) -> Result<(usize, ProtoHandler), String> { - pktsum.l2_dst = get_field(pkt, offset, 6).ok(); - pktsum.l2_src = get_field(pkt, offset + 6, 6).ok(); - pktsum.ethertype = get_field(pkt.data, offset + 12, 2).map(|x| x as u16).ok(); + pktsum.l2_dst = get_field!(pkt, offset, 6, u64).ok(); + pktsum.l2_src = get_field!(pkt, offset + 6, 6, u64).ok(); + pktsum.ethertype = get_field!(pkt.data, offset + 12, 2, u16).ok(); let next_proto_hdl = get_ethertype_handler(&pktsum.ethertype); @@ -143,10 +155,8 @@ pub fn handle_vlan( offset: usize, pktsum: &mut PacketSummary, ) -> Result<(usize, ProtoHandler), String> { - pktsum.vlan_id = get_field(pkt.data, offset, 2) - .map(|x| x as u16 & 0xfff) - .ok(); - pktsum.ethertype = get_field(pkt.data, offset + 2, 2).map(|x| x as u16).ok(); + pktsum.vlan_id = get_field!(pkt.data, offset, 2, u16).map(|x| x & 0xfff).ok(); + pktsum.ethertype = get_field!(pkt.data, offset + 2, 2, u16).ok(); let next_proto_hdl = get_ethertype_handler(&pktsum.ethertype); @@ -161,8 +171,8 @@ pub fn handle_ipv4( let ihl = ((pkt.data[offset] & 0xf) * 4) as usize; pktsum.next_proto = Some(pkt.data[offset + 9]); - pktsum.l3_src = get_field(pkt, offset + 12, 4).ok(); - pktsum.l3_dst = get_field(pkt, offset + 16, 4).ok(); + pktsum.l3_src = get_field!(pkt, offset + 12, 4, u128).ok(); + pktsum.l3_dst = get_field!(pkt, offset + 16, 4, u128).ok(); let next_proto_hdl = get_nextproto_handler(&pktsum.next_proto); @@ -177,8 +187,8 @@ pub fn handle_ipv6( let mut next_offset = offset + 40; let mut next_proto = pkt.data[offset + 6]; - pktsum.l3_src = get_field(pkt, offset + 8, 16).ok(); - pktsum.l3_dst = get_field(pkt, offset + 24, 16).ok(); + pktsum.l3_src = get_field!(pkt, offset + 8, 16, u128).ok(); + pktsum.l3_dst = get_field!(pkt, offset + 24, 16, u128).ok(); // walk until we hit bottom of IPv6 header stack let mut bos = false; @@ -189,8 +199,8 @@ pub fn handle_ipv6( next_offset += 8 + (pkt[next_offset + 1] * 8) as usize; } IPV6_FRAG => { - let frag = get_field(pkt, next_offset + 2, 2) - .map(|x| x as u16 & 0xff8) + let frag = get_field!(pkt, next_offset + 2, 2, u16) + .map(|x| x & 0xff8) .unwrap(); next_proto = pkt[next_offset]; next_offset += 8; @@ -233,8 +243,8 @@ pub fn handle_ports( ) -> Result<(usize, ProtoHandler), String> { let sport_offset = offset; let dport_offset = offset + 2; - pktsum.l4_sport = get_field(pkt.data, sport_offset, 2).ok().map(|x| x as u16); - pktsum.l4_dport = get_field(pkt.data, dport_offset, 2).ok().map(|x| x as u16); + pktsum.l4_sport = get_field!(pkt.data, sport_offset, 2, u16).ok(); + pktsum.l4_dport = get_field!(pkt.data, dport_offset, 2, u16).ok(); Ok((offset + 4, ProtoHandler::COMPLETE)) } @@ -387,12 +397,12 @@ impl<'a> PacketSummary<'a> { } out.push_str(format!("({})", next_proto.green()).as_str()); } else { - // create with 17 byte capacity as this will be fixed len + // create with 17 char capacity as this will be fixed len let mut l2_src = String::with_capacity(17); let mut l2_dst = String::with_capacity(17); - int_to_mac_str(&(self.l2_src.unwrap_or(0) as u64), &mut l2_src); - int_to_mac_str(&(self.l2_dst.unwrap_or(0) as u64), &mut l2_dst); + int_to_mac_str(&(self.l2_src.unwrap_or(0)), &mut l2_src); + int_to_mac_str(&(self.l2_dst.unwrap_or(0)), &mut l2_dst); let mut ethertype = "----".to_string(); if let Some(et) = self.ethertype { @@ -507,9 +517,9 @@ mod tests { #[test] fn test_get_field() { let data = &[0x01, 0x23, 0x34, 0x0f, 0xff, 0x56]; - let expected = 4095; - let result = get_field(data, 3, 2); + let expected: u16 = 4095; + let result = get_field!(data, 3, 2, u16); assert!(result.is_ok()); assert_eq!(result.unwrap(), expected); } diff --git a/src/lib/strings.rs b/src/lib/strings.rs index 169049a..11dde84 100644 --- a/src/lib/strings.rs +++ b/src/lib/strings.rs @@ -32,8 +32,9 @@ pub fn dump_strings( } } - let mut printed = false; + let mut found = false; let mut chars = 0; + let mut display_string = String::new(); let mut partial = String::new(); let mut pkt_str: Option = None; for byte in pkt.data { @@ -42,7 +43,7 @@ pub fn dump_strings( if c.is_ascii() && !c.is_ascii_control() { chars += 1; if chars > *len { - print!("{}", c); + display_string.push(c); } else { partial.push(c); if chars == *len { @@ -57,28 +58,33 @@ pub fn dump_strings( } let idx = pkt_count.to_string().blue(); - if !printed || !*block_print { + if !found || !*block_print { if let Some(ref mut pkt_str) = pkt_str { - print!("[{idx}]{pkt_str}: "); - printed = true; + display_string.push_str(format!("[{idx}]{pkt_str}: ").as_str()); + found = true; if *block_print { - println!(); + display_string.push('\n'); } } } - print!("{partial}"); + display_string.push_str(partial.as_str()); + partial.clear(); } } } else { + // print when we encounter non-ascii if chars >= *len { - println!(); + println!("{}", display_string); + } else { + partial.clear(); + display_string.clear() } chars = 0; - partial.clear(); } } + // print if we hit end of packet but havent dumped buffer yet if chars >= *len { - println!(); + println!("{}", display_string); } } }