Files
scpcap/src/cli.rs
T
ericandClaude Sonnet 5.5 a9e283a87c Add --mac filter and rename IpFilter to PacketFilter
A packet is kept if its Ethernet source or destination MAC matches any
--mac entry; combined with --ip, both must match. Empty dimensions match
everything, so the unfiltered case is still the same code path.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-13 14:48:51 -05:00

168 lines
6.2 KiB
Rust

use clap::Parser;
/// scp-like tool for copying (possibly still-growing) pcap capture files.
#[derive(Parser, Debug)]
#[command(name = "scpcap")]
pub struct ClientCli {
/// Source: local path or [user@]host:path. If the name ends in the
/// partial extension, it's treated as still growing.
pub source: String,
/// Destination: local path or [user@]host:path.
pub dest: String,
/// Compress in flight; destination is written as a streaming .pcap.zst.
/// Only "zstd" is supported.
#[arg(long, value_name = "CODEC", num_args = 0..=1, default_missing_value = "zstd", require_equals = true)]
pub compress: Option<String>,
/// Target chunk size for compression, e.g. "256k", "1m".
#[arg(long, default_value = "256k")]
pub chunk: String,
/// zstd compression level (lower = faster; default tuned for throughput
/// to stay competitive with `scp -C`).
#[arg(long, default_value_t = crate::zstd_frame::DEFAULT_LEVEL)]
pub level: i32,
/// Suffix identifying a still-growing file.
#[arg(long, default_value = "partial")]
pub extension: String,
/// Path to the scpcap executable on the remote host, e.g. `~/bin/scpcap`.
#[arg(long, default_value = "scpcap")]
pub remote_exe: String,
/// How long a partial chunk may wait before being sent anyway, bounding
/// latency on a still-growing source: "100ms", "2s", or a bare number of
/// milliseconds. Chunks that reach --chunk sooner are sent immediately;
/// this only governs the leftover. Matters most with --ip, where a
/// selective filter would otherwise take a long time to fill a chunk.
#[arg(long, value_name = "DURATION", default_value = "100ms")]
pub flush_interval: String,
/// Only forward packets whose source or destination IP matches. May be
/// repeated; a packet is kept if it matches any entry. Accepts a bare
/// address ("1.1.1.1", "2606:4700::1111") or a CIDR prefix
/// ("10.0.0.0/8"). Requires an Ethernet-linktype pcap source.
#[arg(long = "ip", value_name = "ADDR")]
pub ip: Vec<String>,
/// Only forward packets whose Ethernet source or destination MAC
/// matches, e.g. to pull one switch connection out of a capture by its
/// upstream MAC ("aa:bb:cc:dd:ee:ff"). May be repeated; a packet is kept
/// if it matches any entry. Combined with --ip, a packet must match
/// both. Requires an Ethernet-linktype pcap source.
#[arg(long = "mac", value_name = "ADDR")]
pub mac: Vec<String>,
/// Append a diagnostic trace of this (local) process's transfer events
/// to this file -- positions, rename detection, finalize -- for
/// figuring out where a stalled transfer is stuck.
#[arg(long, value_name = "PATH")]
pub log: Option<String>,
/// Path (on the remote host) to pass as the spawned `--server`
/// process's own `--log`, so both sides of a remote transfer leave a
/// trace. Ignored for local-to-local transfers.
#[arg(long, value_name = "PATH")]
pub remote_log: Option<String>,
}
/// Remote helper mode, ssh-spawned by the client (like `rsync --server`).
/// Not meant to be invoked directly by a user.
#[derive(Parser, Debug)]
#[command(name = "scpcap --server")]
pub struct ServerCli {
#[command(flatten)]
pub role: ServerRoleFlags,
/// Source path to read (--send mode).
#[arg(long)]
pub source: Option<String>,
/// Source's final (non-partial) path, for rename detection (--send mode).
#[arg(long)]
pub final_source: Option<String>,
/// Whether the source is a still-growing (partial) file (--send mode).
#[arg(long)]
pub partial: bool,
/// Compress in flight (--send mode). No adjacent positional args in
/// --server mode, so (unlike ClientCli's --compress) there's no
/// ambiguity requiring `--compress=zstd` -- `--compress zstd` is fine.
#[arg(long, value_name = "CODEC", num_args = 0..=1, default_missing_value = "zstd")]
pub compress: Option<String>,
/// Target chunk size for compression (--send mode).
#[arg(long, default_value = "256k")]
pub chunk: String,
/// zstd compression level (--send mode).
#[arg(long, default_value_t = crate::zstd_frame::DEFAULT_LEVEL)]
pub level: i32,
/// Flush interval for a partial chunk (--send mode). See the client's
/// --flush-interval.
#[arg(long, value_name = "DURATION", default_value = "100ms")]
pub flush_interval: String,
/// IP filter for --send mode; repeated, one per address/prefix. See the
/// client's --ip.
#[arg(long = "ip", value_name = "ADDR")]
pub ip: Vec<String>,
/// MAC filter for --send mode; repeated, one per address. See the
/// client's --mac.
#[arg(long = "mac", value_name = "ADDR")]
pub mac: Vec<String>,
/// Destination final path (--recv mode).
#[arg(long)]
pub dest_final: Option<String>,
/// Destination temp path, written while in flight (--recv mode).
#[arg(long)]
pub dest_temp: Option<String>,
/// Append a diagnostic trace of this (remote) process's transfer events
/// to this file. See the client's `--log`/`--remote-log`.
#[arg(long, value_name = "PATH")]
pub log: Option<String>,
}
/// The clap-facing shape of the role choice: two mutually exclusive,
/// individually-long-named flags, matching rsync's `--server --send`/
/// `--server --recv` convention. Clap's derive has no direct way to map two
/// separate boolean flags onto one enum field, so this stays a flag pair at
/// the parsing boundary and converts to `ServerRole` via `role()` below.
#[derive(clap::Args, Debug)]
#[group(required = true, multiple = false)]
pub struct ServerRoleFlags {
#[arg(long)]
pub send: bool,
#[arg(long)]
pub recv: bool,
}
/// The role a `--server` process plays, for the rest of the code to match on
/// instead of checking two booleans.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ServerRole {
Send,
Recv,
}
impl ServerCli {
/// The `--group(required, exclusive)` on `ServerRoleFlags` guarantees
/// exactly one of `send`/`recv` is set by the time parsing succeeds.
pub fn role(&self) -> ServerRole {
if self.role.send {
ServerRole::Send
} else {
ServerRole::Recv
}
}
}