Implement native PacketDump capture (#87)
Some checks failed
Native Rust workspace compile / compile (push) Has been cancelled
Some checks failed
Native Rust workspace compile / compile (push) Has been cancelled
This commit is contained in:
215
programs/tests/packet_dump_cli.rs
Normal file
215
programs/tests/packet_dump_cli.rs
Normal file
@@ -0,0 +1,215 @@
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Output, Stdio};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
const USE_CIRCUIT_CODE: &str = concat!(
|
||||
"400000000000ffff000344332211",
|
||||
"000102030405060708090a0b0c0d0e0f",
|
||||
"101112131415161718191a1b1c1d1e1f"
|
||||
);
|
||||
const UNKNOWN_PACKET: &str = "400000000000fe";
|
||||
const EXIT_USAGE: i32 = 2;
|
||||
const EXIT_INPUT: i32 = 3;
|
||||
const EXIT_OUTPUT: i32 = 5;
|
||||
|
||||
static TEMP_ID: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
struct TestDir(PathBuf);
|
||||
|
||||
impl TestDir {
|
||||
fn new(test_name: &str) -> Self {
|
||||
let id = TEMP_ID.fetch_add(1, Ordering::Relaxed);
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"metacrate-packet-dump-{test_name}-{}-{id}",
|
||||
std::process::id()
|
||||
));
|
||||
fs::create_dir(&path).expect("create isolated test directory");
|
||||
Self(path)
|
||||
}
|
||||
|
||||
fn path(&self, name: &str) -> PathBuf {
|
||||
self.0.join(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn run(args: &[&str]) -> Output {
|
||||
Command::new(env!("CARGO_BIN_EXE_packet-dump"))
|
||||
.args(args)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.expect("run packet-dump")
|
||||
}
|
||||
|
||||
fn text(bytes: &[u8]) -> &str {
|
||||
std::str::from_utf8(bytes).expect("command output is UTF-8")
|
||||
}
|
||||
|
||||
fn path_text(path: &Path) -> &str {
|
||||
path.to_str().expect("test path is UTF-8")
|
||||
}
|
||||
|
||||
fn assert_exit(output: &Output, expected: i32) {
|
||||
assert_eq!(
|
||||
output.status.code(),
|
||||
Some(expected),
|
||||
"stdout:\n{}\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
fn script(directory: &TestDir) -> PathBuf {
|
||||
let path = directory.path("packets.tsv");
|
||||
fs::write(
|
||||
&path,
|
||||
format!(
|
||||
"# direction, simulator, and wire bytes\n\
|
||||
incoming\tAlpha Region\t{USE_CIRCUIT_CODE}\n\
|
||||
outgoing\tBeta Region\t{USE_CIRCUIT_CODE}\n\
|
||||
incoming\tAlpha Region\t{UNKNOWN_PACKET}\n"
|
||||
),
|
||||
)
|
||||
.expect("write fake packet script");
|
||||
path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_preserves_upstream_arguments_and_documents_safe_capture_controls() {
|
||||
let output = run(&["--help"]);
|
||||
assert!(output.status.success());
|
||||
let help = text(&output.stdout);
|
||||
for marker in [
|
||||
"[FIRSTNAME]",
|
||||
"[LASTNAME]",
|
||||
"[PASSWORD]",
|
||||
"[SECONDS]",
|
||||
"--fake-script",
|
||||
"--direction",
|
||||
"--packet-type",
|
||||
"--raw",
|
||||
"--output",
|
||||
"--max-output-bytes",
|
||||
"--max-packets",
|
||||
"incoming<TAB>simulator<TAB>hex-bytes",
|
||||
] {
|
||||
assert!(help.contains(marker), "help omitted {marker}:\n{help}");
|
||||
}
|
||||
|
||||
let output = run(&[]);
|
||||
assert_exit(&output, EXIT_USAGE);
|
||||
assert!(text(&output.stderr).contains("Usage: packet-dump"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fake_capture_decodes_directions_types_headers_raw_bytes_and_malformed_data() {
|
||||
let directory = TestDir::new("capture");
|
||||
let script = script(&directory);
|
||||
let output = run(&["--fake-script", path_text(&script), "--raw"]);
|
||||
assert!(output.status.success(), "{}", text(&output.stderr));
|
||||
assert!(output.stderr.is_empty());
|
||||
let output = text(&output.stdout);
|
||||
assert!(output.contains(
|
||||
"IN type=UseCircuitCode simulator=Alpha Region bytes=46 sequence=0 frequency=Low id=3 reliable=true resent=false zerocoded=false appended_acks=false"
|
||||
));
|
||||
assert!(output.contains(&format!("raw={USE_CIRCUIT_CODE}")));
|
||||
assert!(output.contains("OUT type=UseCircuitCode simulator=Beta Region bytes=46"));
|
||||
assert!(output.contains("IN malformed-or-unknown simulator=Alpha Region bytes=7"));
|
||||
assert!(output.contains("Capture complete; packets=3 dropped=0 active_tasks=0 open_sockets=0"));
|
||||
|
||||
let output = run(&[
|
||||
"--fake-script",
|
||||
path_text(&script),
|
||||
"--direction",
|
||||
"incoming",
|
||||
"--packet-type",
|
||||
"UseCircuitCode",
|
||||
]);
|
||||
assert!(output.status.success());
|
||||
let output = text(&output.stdout);
|
||||
assert!(output.contains("IN type=UseCircuitCode"));
|
||||
assert!(!output.contains("OUT type="));
|
||||
assert!(!output.contains("malformed-or-unknown"));
|
||||
assert!(output.contains("Capture complete; packets=1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_file_packet_and_byte_limits_are_enforced_without_unbounded_writes() {
|
||||
let directory = TestDir::new("limits");
|
||||
let script = script(&directory);
|
||||
let capture = directory.path("capture.log");
|
||||
let output = run(&[
|
||||
"--fake-script",
|
||||
path_text(&script),
|
||||
"--max-packets",
|
||||
"1",
|
||||
"--output",
|
||||
path_text(&capture),
|
||||
]);
|
||||
assert!(output.status.success(), "{}", text(&output.stderr));
|
||||
assert!(output.stdout.is_empty());
|
||||
let saved = fs::read_to_string(capture).expect("read capture output");
|
||||
assert!(saved.contains("Packet limit reached"));
|
||||
assert!(saved.contains("Capture complete; packets=1"));
|
||||
|
||||
let limited = directory.path("limited.log");
|
||||
let output = run(&[
|
||||
"--fake-script",
|
||||
path_text(&script),
|
||||
"--max-output-bytes",
|
||||
"10",
|
||||
"--output",
|
||||
path_text(&limited),
|
||||
]);
|
||||
assert_exit(&output, EXIT_OUTPUT);
|
||||
assert!(text(&output.stderr).contains("output byte limit"));
|
||||
assert_eq!(
|
||||
fs::metadata(limited).expect("limited file metadata").len(),
|
||||
0
|
||||
);
|
||||
|
||||
let output = run(&["--fake-script", path_text(&script), "--max-packets", "0"]);
|
||||
assert_exit(&output, EXIT_USAGE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_scripts_filters_and_credentials_fail_without_leaking_secrets() {
|
||||
let directory = TestDir::new("invalid");
|
||||
let invalid = directory.path("invalid.tsv");
|
||||
fs::write(&invalid, "incoming\tRegion\tzz\n").expect("write invalid script");
|
||||
let output = run(&["--fake-script", path_text(&invalid)]);
|
||||
assert_exit(&output, EXIT_INPUT);
|
||||
assert!(output.stdout.is_empty());
|
||||
assert!(text(&output.stderr).contains("non-hex digit"));
|
||||
|
||||
let valid = script(&directory);
|
||||
let output = run(&[
|
||||
"--fake-script",
|
||||
path_text(&valid),
|
||||
"--packet-type",
|
||||
"NotAPacket",
|
||||
]);
|
||||
assert_exit(&output, EXIT_USAGE);
|
||||
assert!(text(&output.stderr).contains("unknown packet type"));
|
||||
|
||||
let password = "do-not-print-this-password";
|
||||
let output = run(&[
|
||||
"First",
|
||||
"Last",
|
||||
password,
|
||||
"10",
|
||||
"--fake-script",
|
||||
path_text(&valid),
|
||||
]);
|
||||
assert_exit(&output, EXIT_USAGE);
|
||||
assert!(!text(&output.stdout).contains(password));
|
||||
assert!(!text(&output.stderr).contains(password));
|
||||
}
|
||||
Reference in New Issue
Block a user