summary history branches tags files
commit:723fd612708434c285e40e4208c50803015d92e6
author:Trevor Bentley
committer:Trevor Bentley
date:Fri May 3 18:10:52 2024 +0200
parents:
usbmon filtering tool with some examples
diff --git a/.gitignore b/.gitignore
line changes: +1/-0
index 0000000..ea8c4bf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/target

diff --git a/Cargo.lock b/Cargo.lock
line changes: +345/-0
index 0000000..23d51f2
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,345 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
+dependencies = [
+ "anstyle",
+ "windows-sys",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
+
+[[package]]
+name = "bitflags"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
+
+[[package]]
+name = "clap"
+version = "4.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap-num"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e063d263364859dc54fb064cedb7c122740cd4733644b14b176c097f51e8ab7"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
+
+[[package]]
+name = "libc"
+version = "0.2.154"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
+
+[[package]]
+name = "memchr"
+version = "2.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
+
+[[package]]
+name = "menomonmon"
+version = "0.1.0"
+dependencies = [
+ "clap",
+ "clap-num",
+ "clap_derive",
+ "nix",
+ "regex",
+]
+
+[[package]]
+name = "nix"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "regex"
+version = "1.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "2.0.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"

diff --git a/Cargo.toml b/Cargo.toml
line changes: +22/-0
index 0000000..0c25386
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "menomonmon"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+
+[lib]
+name = "menomonmon"
+path = "src/lib.rs"
+
+[[bin]]
+name = "menomonmon"
+path = "src/main.rs"
+
+[dependencies]
+clap_derive = "4.5.4"
+clap-num = "1.1.1"
+clap = { version="4.5.4", features=["derive"] }
+nix = {version = "0.28", features = ["ioctl"]}
+regex = "1.10"

diff --git a/examples/mon_fido_enumeration.rs b/examples/mon_fido_enumeration.rs
line changes: +41/-0
index 0000000..86e1647
--- /dev/null
+++ b/examples/mon_fido_enumeration.rs
@@ -0,0 +1,41 @@
+use menomonmon::{
+    monitor,
+    CliArgs,
+    UsbmonFilter,
+};
+
+
+fn main() {
+    // get optional device filter from command-line args
+    let dev_filter = CliArgs::dev_filter();
+
+    // Start outputting packets when the device descriptor is fetched.
+    // This is the first thing linux does when a device is plugged in.
+    let start_filter = UsbmonFilter {
+        epnum: Some(|x: u8| x == 0x00),              // EP 0
+        setup_request_type: Some(|x: u8| x == 0x80), // standard request
+        setup_request: Some(|x: u8| x == 0x06),      // get descriptor
+        setup_value: Some(|x: u16| x == 0x0100),     // device descriptor
+        setup_length: Some(|x: u16| x == 18),        // 18-byte request
+        ..Default::default()
+    };
+
+    // Stop outputting packets when the FIDO HID Report descriptor is
+    // fetched.  This is approximately the last thing linux does when
+    // enumerating a FIDO/U2F device.
+    let end_filter = UsbmonFilter {
+        epnum: Some(|x: u8| x == 0x00),              // EP 0
+        setup_request_type: Some(|x: u8| x == 0x81), // standard request
+        setup_request: Some(|x: u8| x == 0x06),      // get descriptor
+        setup_value: Some(|x: u16| x == 0x2200),     // HID Report
+        setup_index: Some(|x: u16| x == 0x0001),     // FIDO descriptor
+        pkts_after: 1,                               // get reply
+        ..Default::default()
+    };
+
+    // output all packets for the selected device between the two
+    // filter events.
+    let usec = monitor(Some(&dev_filter), Some(&start_filter), None, Some(&end_filter));
+
+    println!("FIDO device enumerated in: {usec} µs")
+}

diff --git a/examples/mon_fido_enumeration_data_only.rs b/examples/mon_fido_enumeration_data_only.rs
line changes: +49/-0
index 0000000..2c08d74
--- /dev/null
+++ b/examples/mon_fido_enumeration_data_only.rs
@@ -0,0 +1,49 @@
+use menomonmon::{
+    monitor,
+    CliArgs,
+    UsbmonFilter,
+};
+use nix::libc;
+
+
+fn main() {
+    // get optional device filter from command-line args
+    let dev_filter = CliArgs::dev_filter();
+
+    // Start outputting packets when the device descriptor is fetched.
+    // This is the first thing linux does when a device is plugged in.
+    let start_filter = UsbmonFilter {
+        epnum: Some(|x: u8| x == 0x00),              // EP 0
+        setup_request_type: Some(|x: u8| x == 0x80), // standard request
+        setup_request: Some(|x: u8| x == 0x06),      // get descriptor
+        setup_value: Some(|x: u16| x == 0x0100),     // device descriptor
+        setup_length: Some(|x: u16| x == 18),        // 18-byte request
+        ..Default::default()
+    };
+
+    // Stop outputting packets when the FIDO HID Report descriptor is
+    // fetched.  This is approximately the last thing linux does when
+    // enumerating a FIDO/U2F device.
+    let end_filter = UsbmonFilter {
+        epnum: Some(|x: u8| x == 0x00),              // EP 0
+        setup_request_type: Some(|x: u8| x == 0x81), // standard request
+        setup_request: Some(|x: u8| x == 0x06),      // get descriptor
+        setup_value: Some(|x: u16| x == 0x2200),     // HID Report
+        setup_index: Some(|x: u16| x == 0x0001),     // FIDO descriptor
+        pkts_after: 1,                               // get reply
+        ..Default::default()
+    };
+
+    // Only output data packets
+    let active_filter = UsbmonFilter {
+        data_len: Some(|x: libc::c_uint| x > 0),
+        ..Default::default()
+    };
+
+    // output all packets for the selected device between the two
+    // filter events.
+    let usec = monitor(Some(&dev_filter), Some(&start_filter),
+                       Some(&active_filter), Some(&end_filter));
+
+    println!("FIDO device enumerated in: {usec} µs")
+}

diff --git a/src/lib.rs b/src/lib.rs
line changes: +629/-0
index 0000000..7b52ef6
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,629 @@
+use clap::Parser;
+use clap_num::maybe_hex;
+use regex::Regex;
+use std::fs;
+use std::os::fd::AsRawFd;
+use std::vec::Vec;
+use nix::{self, request_code_write};
+use nix::libc;
+
+const DATA_MAX: usize = 4096;
+
+const MON_IOC_MAGIC: u8 = 0x92;
+const MON_IOCQ_URB_LEN: u8 = 1;
+const MON_IOCG_STATS: u8 = 3;
+const MON_IOCX_GETX: u8 = 10;
+
+#[derive(Parser, Debug, Default)]
+#[command(author = "Trevor Bentley", version, about, long_about = None)]
+#[command(help_template = "\
+{name} v{version}, by {author-with-newline}
+{about-with-newline}
+{usage-heading} {usage}
+
+{all-args}{after-help}
+")]
+pub struct CliArgs {
+    /// Which USB bus to monitor (optional.  Default: all)
+    #[arg(short, long)]
+    pub bus: Option<u16>,
+    /// USB device Vendor ID to monitor (optional.  Default: all)
+    #[arg(short, long, value_parser=maybe_hex::<u16>)]
+    pub vid: Option<u16>,
+    /// USB device Product ID to monitor (optional.  Default: all)
+    #[arg(short, long, value_parser=maybe_hex::<u16>)]
+    pub pid: Option<u16>,
+    /// Suppress packet output
+    #[arg(short, long)]
+    pub quiet: bool,
+    /// Show field header
+    #[arg(long)]
+    pub header: bool,
+}
+impl CliArgs {
+    pub fn args() -> CliArgs {
+        CliArgs::parse()
+    }
+    pub fn dev_filter() -> DeviceFilter {
+        let cli = CliArgs::args();
+        DeviceFilter {
+            bus: cli.bus,
+            vid: cli.vid,
+            pid: cli.pid,
+        }
+    }
+}
+
+pub type U8Cmp = fn(x: u8) -> bool;
+pub type U16Cmp = fn(x: u16) -> bool;
+pub type CIntCmp = fn(x: libc::c_int) -> bool;
+pub type CUintCmp = fn(x: libc::c_uint) -> bool;
+
+#[derive(Default, Copy, Clone)]
+pub struct DeviceFilter {
+    pub bus: Option<u16>,
+    pub vid: Option<u16>,
+    pub pid: Option<u16>,
+}
+
+#[derive(Default, Copy, Clone)]
+#[allow(dead_code)]
+pub struct UsbmonFilter {
+    // Capture behavior
+    pub pkts_after: usize,    // packets to output after this packet
+
+    // USB packet
+    pub pkt_type: Option<U8Cmp>,
+    pub xfer_type: Option<U8Cmp>,
+    pub epnum: Option<U8Cmp>,
+    pub devnum: Option<U8Cmp>,
+    pub busnum: Option<U16Cmp>,
+    pub flag_setup: Option<U8Cmp>,
+    pub flag_data: Option<U8Cmp>,
+    pub status: Option<CIntCmp>,
+    pub data_len: Option<CUintCmp>,
+
+    // Setup packet
+    pub setup_request_type: Option<U8Cmp>,
+    pub setup_request: Option<U8Cmp>,
+    pub setup_value: Option<U16Cmp>,
+    pub setup_index: Option<U16Cmp>,
+    pub setup_length: Option<U16Cmp>,
+}
+
+
+#[derive(Default)]
+pub struct UsbmonDevice {
+    bus: u16,
+    dev: u8,
+
+    vendor_id: u16,
+    product_id: u16,
+    manufacturer: String,
+    product: String,
+}
+impl UsbmonDevice {
+    pub fn device_match(&self, bus: u16, devnum: Option<u8>,
+                        vid: Option<u16>, pid: Option<u16>) -> bool {
+        let mut is_match = true;
+        is_match &= bus == 0 || self.bus == bus;
+        is_match &= devnum.map_or(true, |x| x == self.dev);
+        is_match &= vid.map_or(true, |x| x == self.vendor_id);
+        is_match &= pid.map_or(true, |x| x == self.product_id);
+        is_match
+    }
+}
+
+pub fn read_devices() -> Vec<UsbmonDevice> {
+    let mut cur_dev: UsbmonDevice = UsbmonDevice::default();
+    let mut all_devs: Vec<UsbmonDevice> = Vec::new();
+    let mon_devices: &str = "/sys/kernel/debug/usb/devices";
+    let contents = fs::read_to_string(mon_devices)
+        .expect("debugfs devices file not found.  Is usbmod kernel loaded?");
+
+    let t_re = Regex::new(r"^T:  Bus=([0-9]*) Lev=([0-9]*) Prnt=([0-9]*) Port=([0-9]*) Cnt=([0-9]*) Dev#=(...) .*$").unwrap();
+    let p_re = Regex::new(r"^P:  Vendor=(....) ProdID=(....).*$").unwrap();
+    let manu_re = Regex::new(r"^S:  Manufacturer=(.*)$").unwrap();
+    let prod_re = Regex::new(r"^S:  Product=(.*)$").unwrap();
+
+    for line in contents.lines() {
+        if let Some(t_caps) = t_re.captures(line) {
+            cur_dev.bus = t_caps.get(1).map_or("0", |s| s.as_str().trim()).parse::<u16>().unwrap_or(0);
+            cur_dev.dev = t_caps.get(6).map_or("0", |s| s.as_str().trim()).parse::<u8>().unwrap_or(0);
+        }
+
+        if let Some(p_caps) = p_re.captures(line) {
+            cur_dev.vendor_id = p_caps.get(1).map_or(0, |s| u16::from_str_radix(s.as_str(), 16).unwrap_or(0));
+            cur_dev.product_id = p_caps.get(2).map_or(0, |s| u16::from_str_radix(s.as_str(), 16).unwrap_or(0));
+        }
+
+        if let Some(manu_caps) = manu_re.captures(line) {
+            cur_dev.manufacturer = manu_caps.get(1).map_or("", |s| s.as_str().trim()).to_owned();
+        }
+
+        if let Some(prod_caps) = prod_re.captures(line) {
+            cur_dev.product = prod_caps.get(1).map_or("", |s| s.as_str().trim()).to_owned();
+            all_devs.push(cur_dev);
+            cur_dev = Default::default();
+        }
+    }
+    all_devs.sort_by_key(|x| x.bus);
+    all_devs
+}
+
+pub fn print_devices(all_devs: &Vec<UsbmonDevice>) {
+    println!("bus  dev   vid:pid    product   [manufacturer]");
+    println!("-----------------------------------------------");
+    for cur_dev in all_devs {
+        println!("{:02}   {:03}   {:04x}:{:04x}   {}   [{}]",
+                 cur_dev.bus, cur_dev.dev,
+                 cur_dev.vendor_id, cur_dev.product_id,
+                 cur_dev.product, cur_dev.manufacturer);
+    }
+}
+
+//struct usbmon_packet {
+//      u64 id;                 /*  0: URB ID - from submission to callback */
+//      unsigned char type;     /*  8: Same as text; extensible. */
+//      unsigned char xfer_type; /*    ISO (0), Intr, Control, Bulk (3) */
+//      unsigned char epnum;    /*     Endpoint number and transfer direction */
+//      unsigned char devnum;   /*     Device address */
+//      u16 busnum;             /* 12: Bus number */
+//      char flag_setup;        /* 14: Same as text */
+//      char flag_data;         /* 15: Same as text; Binary zero is OK. */
+//      s64 ts_sec;             /* 16: gettimeofday */
+//      s32 ts_usec;            /* 24: gettimeofday */
+//      int status;             /* 28: */
+//      unsigned int length;    /* 32: Length of data (submitted or actual) */
+//      unsigned int len_cap;   /* 36: Delivered length */
+//      union {                 /* 40: */
+//              unsigned char setup[SETUP_LEN]; /* Only for Control S-type */
+//              struct iso_rec {                /* Only for ISO */
+//                      int error_count;
+//                      int numdesc;
+//              } iso;
+//      } s;
+//      int interval;           /* 48: Only for Interrupt and ISO */
+//      int start_frame;        /* 52: For ISO */
+//      unsigned int xfer_flags; /* 56: copy of URB's transfer_flags */
+//      unsigned int ndesc;     /* 60: Actual number of ISO descriptors */
+//};                            /* 64 total length */
+
+#[derive(Default, Copy, Clone)]
+#[repr(C, packed)]
+struct IsoRec {
+    error_count: libc::c_int,
+    numdesc: libc::c_int,
+}
+
+#[derive(Default, Copy, Clone)]
+#[allow(non_snake_case)]
+#[repr(C, packed)]
+struct SetupPacket {
+    bmRequestType: u8,
+    bRequest: u8,
+    wValue: u16,
+    wIndex: u16,
+    wLength: u16,
+}
+impl SetupPacket {
+    fn request_type(&self) -> u8 {
+        let x = self.bmRequestType;
+        x
+    }
+    fn request(&self) -> u8 {
+        let x = self.bRequest;
+        x
+    }
+    fn value(&self) -> u16 {
+        let x = self.wValue;
+        x
+    }
+    fn index(&self) -> u16 {
+        let x = self.wIndex;
+        x
+    }
+    fn length(&self) -> u16 {
+        let x = self.wLength;
+        x
+    }
+}
+
+#[derive(Copy, Clone)]
+#[repr(C, packed)]
+union SetupUnion {
+    setup: SetupPacket,
+    iso: IsoRec,
+}
+impl Default for SetupUnion {
+    fn default() -> Self { SetupUnion { setup: Default::default() } }
+}
+
+#[derive(Default, Copy, Clone)]
+#[repr(C, packed)]
+struct UsbmonPacket {
+    id: u64,
+    pkt_type: u8,  // "S" (submit), "C" (complete)
+    xfer_type: u8, // ISO (0), Interrupt (1), Control (2), Bulk (3)
+    epnum: u8,     // direction encoded in highest bit, 0x80 == in
+    devnum: u8,
+    busnum: u16,
+    flag_setup: u8, // 0 or "-" (setup packet present)
+    flag_data: u8,  // 0, ">" (out no data), "=" (out data), or "<" (in)
+    ts_sec: i64,
+    ts_usec: i32,
+    // status always -115 (EINPROGRESS) for submit events
+    status: libc::c_int, // USB URB errno.  -71 == EPROTO, -115 == EINPROGRESS
+    length: libc::c_uint,
+    len_cap: libc::c_uint,
+    s: SetupUnion,
+    interval: libc::c_int,    // IRQ and ISO
+    start_frame: libc::c_int, // ISO
+
+    // from kernel's include/linux/usb.h:
+    //
+    // #define URB_SHORT_NOT_OK        0x0001  /* report short reads as errors */
+    // #define URB_ISO_ASAP            0x0002  /* iso-only; use the first unexpired
+    //                                          * slot in the schedule */
+    // #define URB_NO_TRANSFER_DMA_MAP 0x0004  /* urb->transfer_dma valid on submit */
+    // #define URB_ZERO_PACKET         0x0040  /* Finish bulk OUT with short packet */
+    // #define URB_NO_INTERRUPT        0x0080  /* HINT: no non-error interrupt
+    //                                          * needed */
+    // #define URB_FREE_BUFFER         0x0100  /* Free transfer buffer with the URB */
+    //
+    // /* The following flags are used internally by usbcore and HCDs */
+    // #define URB_DIR_IN              0x0200  /* Transfer from device to host */
+    // #define URB_DIR_OUT             0
+    // #define URB_DIR_MASK            URB_DIR_IN
+    //
+    // #define URB_DMA_MAP_SINGLE      0x00010000      /* Non-scatter-gather mapping */
+    // #define URB_DMA_MAP_PAGE        0x00020000      /* HCD-unsupported S-G */
+    // #define URB_DMA_MAP_SG          0x00040000      /* HCD-supported S-G */
+    // #define URB_MAP_LOCAL           0x00080000      /* HCD-local-memory mapping */
+    // #define URB_SETUP_MAP_SINGLE    0x00100000      /* Setup packet DMA mapped */
+    // #define URB_SETUP_MAP_LOCAL     0x00200000      /* HCD-local setup packet */
+    // #define URB_DMA_SG_COMBINED     0x00400000      /* S-G entries were combined */
+    // #define URB_ALIGNED_TEMP_BUFFER 0x00800000      /* Temp buffer was alloc'd */
+    xfer_flags: libc::c_uint, // bit field of above URB flags
+    ndesc: libc::c_uint, // number of ISO descriptors
+}
+
+impl UsbmonPacket {
+    fn setup_packet(&self) -> Option<&SetupPacket> {
+        match self.flag_setup {
+            b'-' => None,
+            _ => unsafe {
+                let setup = &self.s.setup;
+                Some(setup)
+            },
+        }
+    }
+    fn pkt_type(&self) -> u8 {
+        let x = self.pkt_type;
+        x
+    }
+    fn xfer_type(&self) -> u8 {
+        let x = self.xfer_type;
+        x
+    }
+    fn busnum(&self) -> u16 {
+        let x = self.busnum;
+        x
+    }
+    fn devnum(&self) -> u8 {
+        let x = self.devnum;
+        x
+    }
+    fn epnum(&self) -> u8 {
+        let x = self.epnum;
+        x & 0x7f
+    }
+    fn dir_out(&self) -> bool {
+        let epnum = self.epnum;
+        match epnum & 0x80 { 0 => true, _ => false }
+    }
+    fn dir_chr(&self) -> char {
+        let dir_out = self.dir_out();
+        match dir_out { true => '>', _ => '<' }
+    }
+    fn flag_setup(&self) -> u8 {
+        let x = self.flag_setup;
+        x
+    }
+    fn flag_data(&self) -> u8 {
+        let x = self.flag_data;
+        x
+    }
+    fn status(&self) -> libc::c_int {
+        let x = self.status;
+        x
+    }
+    fn status_chr(&self) -> char {
+        match self.pkt_type() {
+            b'S' => ' ', // submit packets always have status EINPROGRESS
+            _ => match self.status {
+                0 => ' ',
+                _ => 'e',
+            },
+        }
+    }
+    fn length(&self) -> libc::c_uint {
+        let x = self.len_cap;
+        x
+    }
+    fn length_available(&self) -> libc::c_uint {
+        std::cmp::min(self.length(), DATA_MAX as u32)
+    }
+    fn ts_sec(&self) -> i64 {
+        let x = self.ts_sec;
+        x
+    }
+    fn ts_usec(&self) -> i64 {
+        let x = self.ts_usec;
+        x as i64
+    }
+    fn usec_since(&self, since: i64) -> i64 {
+        let ts_usec = (self.ts_sec() * 1000 * 1000) + (self.ts_usec());
+        ts_usec - since
+    }
+
+    fn is_error(&self) -> bool {
+        // 'S' packets always return EINPROGRESS
+        self.pkt_type != b'S' && self.status != 0
+    }
+
+    fn match_filter(&self, f: Option<&UsbmonFilter>) -> bool {
+        let mut matched = true;
+        if f.is_none() {
+            return true;
+        }
+        let f = f.unwrap();
+
+        matched &= f.pkt_type.map(|x| x(self.pkt_type())).unwrap_or(true);
+        matched &= f.xfer_type.map(|x| x(self.xfer_type())).unwrap_or(true);
+        matched &= f.epnum.map(|x| x(self.epnum())).unwrap_or(true);
+        matched &= f.devnum.map(|x| x(self.devnum())).unwrap_or(true);
+        matched &= f.busnum.map(|x| x(self.busnum())).unwrap_or(true);
+        matched &= f.flag_setup.map(|x| x(self.flag_setup())).unwrap_or(true);
+        matched &= f.flag_data.map(|x| x(self.flag_data())).unwrap_or(true);
+        matched &= f.status.map(|x| x(self.status())).unwrap_or(true);
+        matched &= f.data_len.map(|x| x(self.length())).unwrap_or(true);
+
+        matched &= f.setup_request_type
+            .map(|x| x(self.setup_packet()
+                       .map(|s| s.request_type()).unwrap_or(0))).unwrap_or(true);
+        matched &= f.setup_request
+            .map(|x| x(self.setup_packet()
+                       .map(|s| s.request()).unwrap_or(0))).unwrap_or(true);
+        matched &= f.setup_value
+            .map(|x| x(self.setup_packet()
+                       .map(|s| s.value()).unwrap_or(0))).unwrap_or(true);
+        matched &= f.setup_index
+            .map(|x| x(self.setup_packet()
+                       .map(|s| s.index()).unwrap_or(0))).unwrap_or(true);
+        matched &= f.setup_length
+            .map(|x| x(self.setup_packet()
+                       .map(|s| s.length()).unwrap_or(0))).unwrap_or(true);
+
+        matched
+    }
+
+    fn print_hdr() {
+        println!("   usec    bs:dv:ep dir pkt:xfr  [len]: |setup| data");
+        println!("------------------------------------------------------------------------");
+    }
+    fn print(&self, start_time: i64, pkt_data: &[u8; DATA_MAX]) {
+        print!("{:010} {:02}:{:02}:{:02}  {}   {}:{:02} {} [{:03}]: ",
+               self.usec_since(start_time),
+               self.busnum(),
+               self.devnum(),
+               self.epnum(),
+               self.dir_chr(),
+               self.pkt_type(),
+               self.xfer_type(),
+               self.status_chr(),
+               self.length());
+        if let Some(setup_pkt) = self.setup_packet() {
+            print!("|{:02x} {:02x} {:04x} {:04x} {:02}| ",
+                   setup_pkt.request_type(),
+                   setup_pkt.request(),
+                   setup_pkt.value(),
+                   setup_pkt.index(),
+                   setup_pkt.length());
+        }
+        // print data
+        if self.length() > 0 {
+            for i in (0..self.length_available()).step_by(16) {
+                if i != 0 || self.setup_packet().is_some() {
+                    println!();
+                    print!("                                        ")
+                }
+                for j in 0..16 {
+                    print!("{:02x}", pkt_data[(i + j) as usize]);
+                }
+            }
+        }
+        println!();
+    }
+}
+
+#[derive(Default, Copy, Clone)]
+#[repr(C, packed)]
+struct MonStats {
+    queued: u32,
+    dropped: u32,
+}
+
+#[derive(Copy, Clone)]
+#[repr(C, packed(1))]
+struct MonGetArg {
+    hdr: *mut UsbmonPacket,
+    data: *mut u8,
+    alloc: libc::size_t,
+}
+impl Default for MonGetArg {
+    fn default() -> Self { MonGetArg {
+        hdr: std::ptr::null::<UsbmonPacket>() as *mut UsbmonPacket,
+        data: std::ptr::null::<u8>() as *mut u8,
+        alloc: 0,
+    } }
+}
+
+
+pub fn monitor(dev_filter: Option<&DeviceFilter>,
+               start_filter: Option<&UsbmonFilter>,
+               active_filter: Option<&UsbmonFilter>,
+               end_filter: Option<&UsbmonFilter>) -> i64 {
+    let cli_args = CliArgs::args();
+    let bus: u16 = match dev_filter {
+        None => 0,
+        Some(f) => f.bus.unwrap_or(0),
+    };
+    let bus_devfile: String = format!("/dev/usbmon{}", bus);
+    let bus_dev = std::fs::File::open(&bus_devfile)
+        .expect(&format!("device {} not found.  is usbmon module loaded?", &bus_devfile));
+    let bus_dev_fd = bus_dev.as_raw_fd();
+
+    let vid = match dev_filter {
+        None => None,
+        Some(f) => f.vid,
+    };
+    let pid = match dev_filter {
+        None => None,
+        Some(f) => f.pid,
+    };
+
+    nix::ioctl_none!(usbmon_read_len, MON_IOC_MAGIC, MON_IOCQ_URB_LEN);
+    nix::ioctl_read!(usbmon_read_stats, MON_IOC_MAGIC, MON_IOCG_STATS, MonStats);
+
+    // Due to a bug in either the packing or the size_of operator, the
+    // size of 24 bytes has to be explicitly set here to calculate the
+    // ioctl because the struct reports as 32 bytes even when packed.
+    //
+    //nix::ioctl_write_ptr!(usbmon_read_data, MON_IOC_MAGIC, MON_IOCX_GETX, MonGetArg);
+    // request_code_write!(MON_IOC_MAGIC, MON_IOCX_GETX, std::mem::size_of::<MonGetArg>())
+    nix::ioctl_write_ptr_bad!(usbmon_read_data, request_code_write!(MON_IOC_MAGIC, MON_IOCX_GETX, 24), MonGetArg);
+
+    let mut _next_len: i32;
+    let mut mon_stats = Default::default();
+    let mut pkt_data: [u8; DATA_MAX] = [0; DATA_MAX];
+    let mut mon_pkt: UsbmonPacket = Default::default();
+    let mon_arg = MonGetArg {
+        hdr: &mut mon_pkt,
+        data: &mut pkt_data as *mut u8,
+        alloc: DATA_MAX,
+    };
+
+    let mut all_devs: Vec<UsbmonDevice> = read_devices();
+    let mut start_time: i64 = std::time::SystemTime::now()
+        .duration_since(std::time::UNIX_EPOCH)
+        .unwrap_or_default().as_micros() as i64;
+    let last_time: i64;
+
+    let mut capturing: bool = false;
+    let mut stop_capturing_count: Option<usize> = None;
+
+    if cli_args.header {
+        UsbmonPacket::print_hdr();
+    }
+    loop {
+        unsafe { usbmon_read_stats(bus_dev_fd, &mut mon_stats).expect("stats ioctl failed"); }
+        let queued = mon_stats.queued;
+        let _dropped = mon_stats.dropped;
+
+        if queued == 0 {
+            std::thread::sleep(std::time::Duration::from_millis(20));
+            continue;
+        }
+
+        unsafe {
+            // unused, just to prove that ioctl is working
+            _next_len = usbmon_read_len(bus_dev_fd).expect("len ioctl failed");
+
+            // get the actual event
+            usbmon_read_data(bus_dev_fd, &mon_arg).expect("pkt ioctl failed");
+
+            let hdr = mon_arg.hdr;
+
+            // Check if this packet's bus/dev combo has been enumerated yet.  Re-enumerate
+            // if it hasn't or if the packet indicates a bus error (which can be device
+            // connects and disconnects).
+            if !all_devs.iter().any(|d| d.device_match((*hdr).busnum(),
+                                                       Some((*hdr).devnum()),
+                                                       None, None)) ||
+                (*hdr).is_error() {
+                    all_devs = read_devices();
+                }
+
+            // See if we have enumerated a device matching the registered capture filter,
+            // and if this packet matches it.
+            let matching: Vec<&UsbmonDevice> = all_devs.iter().filter(
+                |d| d.device_match(bus, None, vid, pid)).collect();
+            match matching.len() {
+                0 => {
+                    // No known device matching the capture filter.  Maybe it isn't
+                    // connected yet.
+                    continue;
+                },
+                n if n == all_devs.len() => {
+                    // All devices matched means no filter, continue
+                },
+                _ => {
+                    // Multiple devices matched
+                    let mut matched = false;
+                    for dev in matching {
+                        // Device is enumerated, did it produce this packet?
+                        if dev.device_match((*hdr).busnum(),
+                                            Some((*hdr).devnum()),
+                                            dev_filter.map_or(None, |f| f.vid),
+                                            dev_filter.map_or(None, |f| f.pid)) {
+                            matched = true;
+                            break;
+                        }
+                    }
+
+                    if !matched {
+                        // This packet isn't from a matching device.
+                        continue;
+                    }
+
+                    // Packet matches filter, continue.
+                }
+            }
+
+            if !capturing && (*hdr).match_filter(start_filter) {
+                // If the start filter matches, begin capturing packets
+                capturing = true;
+                start_time = (*hdr).usec_since(0);
+                if !cli_args.quiet {
+                    (*hdr).print(start_time, &pkt_data);
+                }
+            }
+            else if capturing {
+                if end_filter.is_some() && (*hdr).match_filter(end_filter) {
+                    if !cli_args.quiet {
+                        (*hdr).print(start_time, &pkt_data);
+                    }
+                    if let Some(f) = end_filter {
+                        stop_capturing_count = Some(f.pkts_after);
+                    }
+                }
+                else if (*hdr).match_filter(active_filter) {
+                    if !cli_args.quiet {
+                        (*hdr).print(start_time, &pkt_data);
+                    }
+                }
+            }
+
+            if let Some(cnt) = stop_capturing_count {
+                if cnt == 0 {
+                    last_time = (*hdr).usec_since(start_time);
+                    break;
+                }
+                let cnt = cnt - 1;
+                stop_capturing_count = Some(cnt);
+            }
+        }
+    }
+    last_time
+}

diff --git a/src/main.rs b/src/main.rs
line changes: +20/-0
index 0000000..d1037d9
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,20 @@
+use menomonmon::{
+    monitor,
+    print_devices,
+    read_devices,
+    CliArgs,
+    UsbmonDevice,
+};
+
+fn main() {
+    // display all connected USB devices
+    let all_devs: Vec<UsbmonDevice> = read_devices();
+    print_devices(&all_devs);
+    println!();
+
+    // get optional device filter from command-line args
+    let dev_filter = CliArgs::dev_filter();
+
+    // print all USB events matching the device(s)
+    monitor(Some(&dev_filter), None, None, None);
+}