use std::path::Path;
use pkttap::{Capture, Dump, Result};
const HELP: &str = "\
record — capture packets to a file
USAGE:
record <SOURCE> <OUTPUT> [OPTIONS]
record -h | --help
ARGUMENTS:
SOURCE Interface name for live capture, or path to a pcap/pcapng file.
OUTPUT Destination file path. Extension selects format:
.pcap — classic pcap
.pcapng — pcapng (preferred; supports multiple interfaces)
OPTIONS:
--filter <EXPR> BPF filter expression (e.g. \"tcp port 443\").
--count <N> Stop after capturing N packets (default: unlimited).
-h, --help Print this help message and exit.
EXAMPLES:
record eth0 out.pcap
record eth0 out.pcap --filter \"tcp\" --count 200
record traffic.pcap filtered.pcapng --filter \"udp port 53\"
";
struct Args {
source: String,
output: String,
filter: Option<String>,
count: Option<u64>,
}
fn parse_args() -> Result<Args> {
let raw: Vec<String> = std::env::args().skip(1).collect();
if raw.is_empty() || raw.iter().any(|a| a == "-h" || a == "--help") {
print!("{HELP}");
std::process::exit(if raw.is_empty() { 1 } else { 0 });
}
if raw.len() < 2 {
eprintln!("error: SOURCE and OUTPUT are both required");
eprintln!("{HELP}");
std::process::exit(1);
}
let source = raw[0].clone();
let output = raw[1].clone();
let mut filter: Option<String> = None;
let mut count: Option<u64> = None;
let mut i = 2;
while i < raw.len() {
match raw[i].as_str() {
"--filter" => {
i += 1;
filter = Some(raw.get(i).cloned().unwrap_or_else(|| {
eprintln!("error: --filter requires a value");
std::process::exit(1);
}));
}
"--count" => {
i += 1;
count = Some(raw.get(i).and_then(|s| s.parse().ok()).unwrap_or_else(|| {
eprintln!("error: --count requires an integer");
std::process::exit(1);
}));
}
other => {
eprintln!("error: unknown argument `{other}`");
std::process::exit(1);
}
}
i += 1;
}
Ok(Args {
source,
output,
filter,
count,
})
}
fn main() {
if let Err(e) = run() {
eprintln!("error: {e}");
std::process::exit(1);
}
}
fn run() -> Result<()> {
let args = parse_args()?;
let filter_str: Option<&str> = args.filter.as_deref();
let mut cap = if Path::new(&args.source).exists() {
Capture::from_file(&args.source).filter(filter_str).open()?
} else {
Capture::live(&args.source)
.promiscuous(true)
.filter(filter_str)
.open()?
};
let link_type = cap.link_type();
eprintln!(
"source: {} link-type: {link_type:?} filter: {}",
args.source,
args.filter.as_deref().unwrap_or("<none>"),
);
let mut dump = Dump::to_file(&args.output).link_type(link_type).open()?;
eprintln!("writing to: {}", args.output);
let limit = args.count.unwrap_or(u64::MAX);
let mut written: u64 = 0;
while written < limit {
let Some(pkt) = cap.next()? else {
break;
};
dump.write_packet(&pkt)?;
written += 1;
if written % 100 == 0 {
eprintln!(" … {written} packets written");
}
}
dump.flush()?;
eprintln!("{written} packets written to {}", args.output);
Ok(())
}