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>
168 lines
6.2 KiB
Rust
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
|
|
}
|
|
}
|
|
}
|