pub mod export;
pub mod replay;
use bpaf::{OptionParser, Parser, construct, long, positional};
use std::path::PathBuf;
use crate::cli::{
export::{ExportArgs, export_cmd},
replay::{ReplayArgs, replay_cmd},
};
#[derive(Debug, Clone)]
pub struct Args {
pub config: Option<PathBuf>,
pub command: Command,
}
#[derive(Debug, Clone)]
pub enum Command {
Info(InfoArgs),
Stats(StatsArgs),
Sort(Box<SortArgs>),
Export(Box<ExportArgs>),
Replay(Box<ReplayArgs>),
}
#[derive(Debug, Clone)]
pub struct InfoArgs {
pub input: PathBuf,
}
#[derive(Debug, Clone)]
pub struct StatsArgs {
pub input: PathBuf,
pub unidirectional: bool,
}
#[derive(Debug, Clone)]
pub struct SortArgs {
pub inputs: Vec<PathBuf>,
pub output: PathBuf,
pub slice: Option<String>,
pub on_disk: bool,
pub proto: Option<String>,
pub src_ip: Vec<String>,
pub dst_ip: Vec<String>,
pub ip: Vec<String>,
pub src_port: Vec<String>,
pub dst_port: Vec<String>,
pub port: Vec<String>,
pub flow_id: Option<String>,
pub from: Option<String>,
pub to: Option<String>,
pub tcp_flags: Option<String>,
pub min_len: Option<u32>,
pub max_len: Option<u32>,
pub unidirectional: bool,
pub negate: bool,
pub filter_expr: Option<String>,
pub min_flow_packets: Option<u64>,
pub max_payload_bytes: Option<u32>,
pub timestamp_start: Option<String>,
pub replace_ip: Vec<String>,
}
fn config_opt() -> impl Parser<Option<PathBuf>> {
long("config")
.short('c')
.help("Path to TOML config file")
.argument::<PathBuf>("FILE")
.optional()
}
fn input_arg() -> impl Parser<PathBuf> {
positional::<PathBuf>("INPUT").help("Input PCAP or PCAPng file")
}
fn info_cmd() -> impl Parser<Command> {
let input = input_arg();
construct!(InfoArgs { input })
.to_options()
.descr("Print a summary of a PCAP capture (packet count, bytes, IPs, timestamps)")
.command("info")
.map(Command::Info)
}
fn stats_cmd() -> impl Parser<Command> {
let input = input_arg();
let unidirectional = long("unidirectional")
.short('u')
.help("Use unidirectional flow IDs instead of bidirectional")
.switch();
construct!(StatsArgs {
unidirectional,
input,
})
.to_options()
.descr("Print per-flow statistics keyed by 5-tuple")
.command("stats")
.map(Command::Stats)
}
fn sort_cmd() -> impl Parser<Command> {
let inputs = positional::<PathBuf>("INPUT")
.help("Input PCAP file(s) — at least one required; multiple files are merged")
.many();
let output = long("output")
.short('o')
.help("Output file (no slicing) or directory (with --slice)")
.argument::<PathBuf>("PATH");
let slice = long("slice")
.short('s')
.help("Split output by time interval, e.g. '1h', '30m', '1d'")
.argument::<String>("DURATION")
.optional();
let on_disk = long("on-disk")
.help("Store the packet index on disk instead of in RAM")
.switch();
let proto = long("proto")
.help("Comma-separated protocols to keep: tcp,udp,icmp,icmp6 or numbers")
.argument::<String>("PROTOS")
.optional();
let src_ip = long("src-ip")
.help("Source IP or CIDR to keep (repeatable, OR-ed)")
.argument::<String>("CIDR")
.many();
let dst_ip = long("dst-ip")
.help("Destination IP or CIDR to keep (repeatable, OR-ed)")
.argument::<String>("CIDR")
.many();
let ip = long("ip")
.help("Either-endpoint IP or CIDR (repeatable, OR-ed)")
.argument::<String>("CIDR")
.many();
let src_port = long("src-port")
.help("Source port or range to keep, e.g. 443 or 1024-65535 (repeatable)")
.argument::<String>("PORT")
.many();
let dst_port = long("dst-port")
.help("Destination port or range to keep (repeatable)")
.argument::<String>("PORT")
.many();
let port = long("port")
.help("Either-endpoint port or range (repeatable)")
.argument::<String>("PORT")
.many();
let flow_id = long("flow-id")
.help("Comma-separated hex flow IDs to retain")
.argument::<String>("IDS")
.optional();
let from = long("from")
.help("Keep packets at or after this datetime (RFC 3339 or Unix epoch seconds)")
.argument::<String>("DATETIME")
.optional();
let to = long("to")
.help("Keep packets at or before this datetime (RFC 3339 or Unix epoch seconds)")
.argument::<String>("DATETIME")
.optional();
let tcp_flags = long("tcp-flags")
.help("TCP flags filter, e.g. SYN, SYN+ACK, RST:exact")
.argument::<String>("FLAGS")
.optional();
let min_len = long("min-len")
.help("Minimum captured packet length in bytes")
.argument::<u32>("BYTES")
.optional();
let max_len = long("max-len")
.help("Maximum captured packet length in bytes")
.argument::<u32>("BYTES")
.optional();
let unidirectional = long("unidirectional")
.short('u')
.help("Compute flow IDs unidirectionally (default: bidirectional)")
.switch();
let negate = long("not")
.help("Invert the filter: keep packets that do NOT match the filter rules")
.switch();
let filter_expr = long("filter")
.short('f')
.help("tcpdump/libpcap BPF expression, e.g. \"tcp and dst port 443\"")
.argument::<String>("EXPR")
.optional();
let min_flow_packets = long("min-flow-packets")
.help("Only include flows with at least N packets (pre-scan pass; non-IP packets excluded)")
.argument::<u64>("N")
.optional();
let max_payload_bytes = long("max-payload-bytes")
.help("Truncate each packet's payload to at most N bytes (headers preserved)")
.argument::<u32>("BYTES")
.optional();
let timestamp_start = long("timestamp-start")
.help("Shift all timestamps so the capture starts at this datetime (RFC 3339 or Unix epoch seconds)")
.argument::<String>("DATETIME")
.optional();
let replace_ip = long("replace-ip")
.help("Replace an IP address: OLD_IP=NEW_IP (repeatable; cross-family IPv4↔IPv6 supported)")
.argument::<String>("MAPPING")
.many();
construct!(SortArgs {
on_disk,
slice,
output,
proto,
src_ip,
dst_ip,
ip,
src_port,
dst_port,
port,
flow_id,
from,
to,
tcp_flags,
min_len,
max_len,
unidirectional,
negate,
filter_expr,
min_flow_packets,
max_payload_bytes,
timestamp_start,
replace_ip,
inputs,
})
.to_options()
.descr("Sort a PCAP capture chronologically using two-pass indexing")
.command("sort")
.map(|a| Command::Sort(Box::new(a)))
}
pub fn parser() -> OptionParser<Args> {
let config = config_opt();
let command = construct!([
info_cmd(),
stats_cmd(),
sort_cmd(),
export_cmd(),
replay_cmd()
]);
construct!(Args { config, command })
.to_options()
.descr("High-performance PCAP inspection, filtering, sorting, and export tool")
.version(env!("CARGO_PKG_VERSION"))
}