use std::io::Write;
use std::process::{Command, Stdio};
use std::time::Duration;
fn bin() -> Command {
Command::new(env!("CARGO_BIN_EXE_sipmon"))
}
fn run_stdin(bytes: Vec<u8>, timeout_s: u64) -> std::process::Output {
let mut child = bin()
.arg("-")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn sipmon");
let _ = child.stdin.take().map(|mut w| w.write_all(&bytes));
let start = std::time::Instant::now();
loop {
if let Ok(Some(status)) = child.try_wait() {
let out = child.wait_with_output().unwrap();
return std::process::Output {
status,
stdout: out.stdout,
stderr: out.stderr,
};
}
if start.elapsed() > Duration::from_secs(timeout_s) {
child.kill().ok();
panic!("sipmon did not exit within {timeout_s}s (possible hang)");
}
std::thread::sleep(Duration::from_millis(50));
}
}
fn not_crash(status: &std::process::ExitStatus) -> bool {
!status.code().map(|c| c < 0 || c == 134).unwrap_or(false)
}
fn build_pcap(frames: Vec<Vec<u8>>) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&0xA1B2C3D4u32.to_le_bytes()); out.extend_from_slice(&2u16.to_le_bytes());
out.extend_from_slice(&4u16.to_le_bytes());
out.extend_from_slice(&0i32.to_le_bytes());
out.extend_from_slice(&0i32.to_le_bytes());
out.extend_from_slice(&65535u32.to_le_bytes());
out.extend_from_slice(&1u32.to_le_bytes()); let ts = 1_800_000_000u32;
for f in frames {
out.extend_from_slice(&ts.to_le_bytes());
out.extend_from_slice(&0u32.to_le_bytes());
out.extend_from_slice(&(f.len() as u32).to_le_bytes());
out.extend_from_slice(&(f.len() as u32).to_le_bytes());
out.extend_from_slice(&f);
}
out
}
fn ether_ip_tcp(payload: &[u8], sport: u16, dport: u16) -> Vec<u8> {
let mut tcp = Vec::new();
tcp.extend_from_slice(&sport.to_be_bytes());
tcp.extend_from_slice(&dport.to_be_bytes());
tcp.extend_from_slice(&1u32.to_be_bytes()); tcp.extend_from_slice(&0u32.to_be_bytes()); tcp.push(0x50); tcp.push(0x18); tcp.extend_from_slice(&65535u16.to_be_bytes()); tcp.extend_from_slice(&0u16.to_be_bytes()); tcp.extend_from_slice(&0u16.to_be_bytes()); tcp.extend_from_slice(payload);
let mut ip = Vec::new();
ip.extend_from_slice(&[0x45, 0x00]);
ip.extend_from_slice(&((20 + tcp.len()) as u16).to_be_bytes());
ip.extend_from_slice(&0u16.to_be_bytes()); ip.extend_from_slice(&0u16.to_be_bytes()); ip.extend_from_slice(&64u8.to_be_bytes());
ip.extend_from_slice(&6u8.to_be_bytes()); ip.extend_from_slice(&0u16.to_be_bytes()); ip.extend_from_slice(&[10, 0, 0, 1]);
ip.extend_from_slice(&[10, 0, 0, 2]);
let mut eth = Vec::new();
eth.extend_from_slice(&[0x02, 0, 0, 0, 0, 1]);
eth.extend_from_slice(&[0x02, 0, 0, 0, 0, 2]);
eth.extend_from_slice(&[0x08, 0x00]);
eth.extend_from_slice(&ip);
eth.extend_from_slice(&tcp);
eth
}
#[test]
fn garbage_pcap_stdin_no_crash() {
let mut rng = 0xdeadbeefu64;
let mut blob = Vec::with_capacity(1_000_000);
for _ in 0..1_000_000 {
rng ^= rng << 13;
rng ^= rng >> 7;
rng ^= rng << 17;
blob.push((rng & 0xff) as u8);
}
let out = run_stdin(blob, 30);
assert!(
not_crash(&out.status),
"garbage stdin caused a crash: {:?}",
out.status
);
}
#[test]
fn truncated_pcap_no_crash() {
let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/pcap_fixtures/sipbot_call.pcap");
let data = std::fs::read(fixture).unwrap();
for cut in [10usize, 100, 1000, 10_000, 100_000, data.len() / 2] {
let out = run_stdin(data[..cut.min(data.len())].to_vec(), 20);
if cut >= 24 {
assert!(
out.status.success(),
"truncated pcap at {cut} should analyze cleanly"
);
} else {
assert!(not_crash(&out.status), "truncated pcap at {cut} crashed");
}
}
}
#[test]
fn huge_content_length_tcp_no_oom() {
let hdr = b"INVITE sip:x SIP/2.0\r\nContent-Length: 2000000000\r\n\r\n".to_vec();
let mut frames = vec![ether_ip_tcp(&hdr, 5060, 5060)];
let chunk = vec![0x41u8; 4096];
for _ in 0..3000 {
frames.push(ether_ip_tcp(&chunk, 5060, 5060));
}
let pcap = build_pcap(frames);
let out = run_stdin(pcap, 60);
assert!(
out.status.success(),
"huge CL must terminate cleanly, not OOM"
);
}
#[test]
fn corrupt_evlog_huge_length_clean_error() {
let dir = tempfile::tempdir().unwrap();
let evlog = dir.path().join("bad.evlog");
let mut data = Vec::new();
data.extend_from_slice(b"SMON");
data.extend_from_slice(&1u16.to_be_bytes());
data.extend_from_slice(&0u16.to_be_bytes());
data.extend_from_slice(&0i32.to_be_bytes());
data.push(1); data.push(1); data.extend_from_slice(&[0xFF; 9]); data.push(0x7F);
std::fs::write(&evlog, data).unwrap();
let start = std::time::Instant::now();
let out = bin()
.args(["query", "-l"])
.arg(&evlog)
.arg("-c")
.arg("x")
.output()
.unwrap();
assert!(start.elapsed() < Duration::from_secs(10));
let _ = out;
}
#[test]
fn nonexistent_inputs_clean_errors() {
for args in [
vec!["file", "-r", "/nonexistent/nope.pcap", "--no-tui"],
vec!["query", "-l", "/nonexistent/nope.evlog", "-c", "x"],
vec!["replay", "-l", "/nonexistent/nope.evlog", "--no-tui"],
] {
let mut c = bin();
for a in &args {
c.arg(a);
}
let start = std::time::Instant::now();
let out = c.output().unwrap();
assert!(
start.elapsed() < Duration::from_secs(10),
"args {args:?} hung"
);
assert!(
!out.status
.code()
.map(|c| c < 0 || c == 134)
.unwrap_or(false),
"args {args:?} crashed: {:?}",
out.status
);
}
}
#[test]
fn random_sip_blast_no_crash() {
let mut frames = Vec::new();
let seeds: &[&[u8]] = &[
b"INVITE sip:a SIP/2.0\r\nCSeq: 1 INVITE\r\nContent-Length: 999999999999\r\n\r\n",
b"SIP/2.0 999 BAD\r\n\r\n",
b"BYE sip:x SIP/2.0\r\nReason: SIP ;cause=99999\r\n\r\n",
b"INVITE sip:a SIP/2.0\r\nContent-Type: application/sdp\r\n\r\nv=0\r\nc=IN IP4 notanip\r\nm=audio 0 RTP/AVP -1\r\n",
];
for s in seeds {
frames.push(ether_ip_tcp(s, 5060, 5060));
}
let pcap = build_pcap(frames);
let out = run_stdin(pcap, 20);
assert!(out.status.success());
}