use std::collections::HashMap;
use std::io;
use std::mem;
use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket};
use std::thread;
use std::time::{Duration, Instant};
use anyhow::{Context, Result};
use pnet::packet::ip::IpNextHeaderProtocols;
use pnet::packet::ipv4::{checksum as ipv4_hdr_cksum, Ipv4Flags, Ipv4Packet, MutableIpv4Packet};
use pnet::packet::tcp::{ipv4_checksum, TcpFlags, TcpPacket};
use pnet::packet::Packet;
use pnet::transport::{
transport_channel, TransportChannelType, TransportReceiver, TransportSender,
};
use pnet_sys;
use rand::Rng;
use std::fmt::Write as _;
use crate::os_fp_db::SubjectFingerprint;
const NUM_SEQ_SAMPLES: usize = 6;
const OS_SEQ_PROBE_DELAY_MS: u64 = 100;
const PRIME_32K: u32 = 32261;
const PRB_OPTS: [&[u8]; 13] = [
b"\x03\x03\x0A\x01\x02\x04\x05\xb4\x08\x0A\xff\xff\xff\xff\x00\x00\x00\x00\x04\x02",
b"\x02\x04\x05\x78\x03\x03\x00\x04\x02\x08\x0A\xff\xff\xff\xff\x00\x00\x00\x00\x00",
b"\x08\x0A\xff\xff\xff\xff\x00\x00\x00\x00\x01\x01\x03\x03\x05\x01\x02\x04\x02\x80",
b"\x04\x02\x08\x0A\xff\xff\xff\xff\x00\x00\x00\x00\x03\x03\x0A\x00",
b"\x02\x04\x02\x18\x04\x02\x08\x0A\xff\xff\xff\xff\x00\x00\x00\x00\x03\x03\x0A\x00",
b"\x02\x04\x01\x09\x04\x02\x08\x0A\xff\xff\xff\xff\x00\x00\x00\x00",
b"\x03\x03\x0A\x01\x02\x04\x05\xb4\x04\x02\x01\x01",
b"\x03\x03\x0A\x01\x02\x04\x01\x09\x08\x0A\xff\xff\xff\xff\x00\x00\x00\x00\x04\x02",
b"\x03\x03\x0A\x01\x02\x04\x01\x09\x08\x0A\xff\xff\xff\xff\x00\x00\x00\x00\x04\x02",
b"\x03\x03\x0A\x01\x02\x04\x01\x09\x08\x0A\xff\xff\xff\xff\x00\x00\x00\x00\x04\x02",
b"\x03\x03\x0A\x01\x02\x04\x01\x09\x08\x0A\xff\xff\xff\xff\x00\x00\x00\x00\x04\x02",
b"\x03\x03\x0A\x01\x02\x04\x01\x09\x08\x0A\xff\xff\xff\xff\x00\x00\x00\x00\x04\x02",
b"\x03\x03\x0f\x01\x02\x04\x01\x09\x08\x0A\xff\xff\xff\xff\x00\x00\x00\x00\x04\x02",
];
const PRB_WIN: [u16; 13] = [1, 63, 4, 4, 16, 512, 3, 128, 256, 1024, 31337, 32768, 65535];
struct TcpReply {
ipid: u16,
ttl: u8,
df: bool,
window: u16,
seq: u32,
ack_num: u32,
flags: u8,
reserved: u8,
urg_ptr: u16,
options_fp: String,
tsval: u32,
data: Vec<u8>,
recv_time: Instant,
}
struct IcmpEchoReply {
ipid: u16,
ttl: u8,
df: bool,
code: u8,
}
struct IcmpUnreachReply {
ttl: u8,
df: bool,
ip_total_len: u16,
unused: u32,
embedded_ip_total_len: u16,
embedded_ip_id: u16,
embedded_ip_cksum: u16,
embedded_ip_cksum_computed: u16,
embedded_udp_cksum: u16,
embedded_udp_data: Vec<u8>,
}
fn local_ipv4() -> io::Result<Ipv4Addr> {
let s = UdpSocket::bind("0.0.0.0:0")?;
s.connect("8.8.8.8:80")?;
match s.local_addr()? {
SocketAddr::V4(v) => Ok(*v.ip()),
_ => Err(io::Error::other("no IPv4 source")),
}
}
fn hex_val(x: u32) -> String {
format!("{:X}", x)
}
fn next_ipid(counter: &mut u16) -> u16 {
let v = *counter;
*counter = counter.wrapping_add(1);
v
}
fn internet_checksum(data: &[u8]) -> u16 {
let mut sum: u32 = 0;
let mut i = 0;
while i + 1 < data.len() {
sum += u16::from_be_bytes([data[i], data[i + 1]]) as u32;
i += 2;
}
if i < data.len() {
sum += (data[i] as u32) << 8;
}
while sum >> 16 != 0 {
sum = (sum >> 16) + (sum & 0xffff);
}
!(sum as u16)
}
fn ttl_guess(ttl: u8) -> u8 {
if ttl <= 32 {
32
} else if ttl <= 64 {
64
} else if ttl <= 128 {
128
} else {
255
}
}
fn flags_str(f: u8) -> String {
let mut s = String::new();
if f & TcpFlags::ECE != 0 {
s.push('E');
}
if f & TcpFlags::URG != 0 {
s.push('U');
}
if f & TcpFlags::ACK != 0 {
s.push('A');
}
if f & TcpFlags::PSH != 0 {
s.push('P');
}
if f & TcpFlags::RST != 0 {
s.push('R');
}
if f & TcpFlags::SYN != 0 {
s.push('S');
}
if f & TcpFlags::FIN != 0 {
s.push('F');
}
s
}
fn quirks_str(reserved: u8, flags: u8, urg: u16) -> String {
let mut s = String::new();
if reserved != 0 {
s.push('R');
}
if urg != 0 && flags & TcpFlags::URG == 0 {
s.push('U');
}
s
}
fn seq_relation(resp_seq: u32, probe_ack: u32) -> String {
if resp_seq == 0 {
"Z".into()
} else if resp_seq == probe_ack {
"A".into()
} else if resp_seq == probe_ack.wrapping_add(1) {
"A+".into()
} else {
"O".into()
}
}
fn ack_relation(resp_ack: u32, probe_seq: u32) -> String {
if resp_ack == 0 {
"Z".into()
} else if resp_ack == probe_seq {
"S".into()
} else if resp_ack == probe_seq.wrapping_add(1) {
"S+".into()
} else {
"O".into()
}
}
fn cc_str(flags: u8) -> String {
let ece = flags & TcpFlags::ECE != 0;
let cwr = flags & TcpFlags::CWR != 0;
match (ece, cwr) {
(true, false) => "Y",
(false, false) => "N",
(true, true) => "S",
_ => "O",
}
.into()
}
fn rd_value(data: &[u8]) -> String {
if data.is_empty() {
"0".into()
} else {
format!("{:X}", crc32fast::hash(data))
}
}
#[allow(clippy::result_unit_err)] pub fn tcp_options_fingerprint(tcp: &TcpPacket<'_>, _mss_hint: u16) -> Result<String, ()> {
let mut out = String::new();
let hdr = tcp.packet();
let data_off = tcp.get_data_offset() as usize * 4;
if data_off < 20 || hdr.len() < data_off {
return Err(());
}
let mut q = &hdr[20..data_off];
while !q.is_empty() && out.len() < 240 {
let opcode = q[0];
if opcode == 0 {
out.push('L');
break;
}
if opcode == 1 {
out.push('N');
q = &q[1..];
continue;
}
if q.len() < 2 {
return Err(());
}
let len = q[1] as usize;
if len < 2 || q.len() < len {
return Err(());
}
match opcode {
2 => {
if len < 4 {
return Err(());
}
let mss = u16::from_be_bytes([q[2], q[3]]);
let _ = write!(&mut out, "M{:X}", mss);
}
3 => {
if len < 3 {
return Err(());
}
let _ = write!(&mut out, "W{:X}", q[2]);
}
4 => {
out.push('S');
}
8 => {
if len < 10 {
return Err(());
}
out.push('T');
let t1 = u32::from_be_bytes([q[2], q[3], q[4], q[5]]);
let t2 = u32::from_be_bytes([q[6], q[7], q[8], q[9]]);
out.push(if t1 != 0 { '1' } else { '0' });
out.push(if t2 != 0 { '1' } else { '0' });
}
_ => {
return Err(());
}
}
q = &q[len..];
}
Ok(out)
}
fn read_timestamp(tcp: &TcpPacket<'_>) -> u32 {
let hdr = tcp.packet();
let data_off = tcp.get_data_offset() as usize * 4;
if data_off <= 20 || hdr.len() < data_off {
return 0;
}
let mut q = &hdr[20..data_off];
while !q.is_empty() {
let op = q[0];
if op == 0 {
break;
}
if op == 1 {
q = &q[1..];
continue;
}
if q.len() < 2 {
break;
}
let len = q[1] as usize;
if len < 2 || q.len() < len {
break;
}
if op == 8 && len >= 10 {
return u32::from_be_bytes([q[2], q[3], q[4], q[5]]);
}
q = &q[len..];
}
0
}
fn recv_ipv4_with_timeout(
tr: &mut TransportReceiver,
t: Duration,
) -> io::Result<Option<Ipv4Packet<'_>>> {
let fd = tr.socket.fd;
let old = pnet_sys::get_socket_receive_timeout(fd)?;
pnet_sys::set_socket_receive_timeout(fd, t)?;
let mut caddr: pnet_sys::SockAddrStorage = unsafe { mem::zeroed() };
let r = pnet_sys::recv_from(fd, &mut tr.buffer[..], &mut caddr);
let _ = pnet_sys::set_socket_receive_timeout(fd, old);
match r {
Ok(len) => Ok(Ipv4Packet::new(&tr.buffer[..len])),
Err(e) if e.kind() == io::ErrorKind::WouldBlock => Ok(None),
Err(e) => Err(e),
}
}
fn recv_tcp_reply(
rx: &mut TransportReceiver,
target: Ipv4Addr,
our_port: u16,
timeout: Duration,
) -> io::Result<Option<TcpReply>> {
let deadline = Instant::now() + timeout;
loop {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Ok(None);
}
let wait = remaining.min(Duration::from_millis(50));
let Some(ip) = recv_ipv4_with_timeout(rx, wait)? else {
continue;
};
if ip.get_source() != target {
continue;
}
if ip.get_next_level_protocol() != IpNextHeaderProtocols::Tcp {
continue;
}
let Some(tcp) = TcpPacket::new(ip.payload()) else {
continue;
};
if tcp.get_destination() != our_port {
continue;
}
let ipid = ip.get_identification();
let ttl = ip.get_ttl();
let df = ip.get_flags() & 0b010 != 0;
let window = tcp.get_window();
let seq = tcp.get_sequence();
let ack_num = tcp.get_acknowledgement();
let flags = tcp.get_flags();
let reserved = tcp.packet()[12] & 0x0F;
let urg_ptr = tcp.get_urgent_ptr();
let options_fp = tcp_options_fingerprint(&tcp, 265).unwrap_or_default();
let tsval = read_timestamp(&tcp);
let data = tcp.payload().to_vec();
return Ok(Some(TcpReply {
ipid,
ttl,
df,
window,
seq,
ack_num,
flags,
reserved,
urg_ptr,
options_fp,
tsval,
data,
recv_time: Instant::now(),
}));
}
}
fn recv_icmp_echo_reply(
rx: &mut TransportReceiver,
target: Ipv4Addr,
expected_id: u16,
timeout: Duration,
) -> io::Result<Option<IcmpEchoReply>> {
let deadline = Instant::now() + timeout;
loop {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Ok(None);
}
let wait = remaining.min(Duration::from_millis(50));
let Some(ip) = recv_ipv4_with_timeout(rx, wait)? else {
continue;
};
if ip.get_source() != target {
continue;
}
if ip.get_next_level_protocol() != IpNextHeaderProtocols::Icmp {
continue;
}
let p = ip.payload();
if p.len() < 8 || p[0] != 0 {
continue;
}
let id = u16::from_be_bytes([p[4], p[5]]);
if id != expected_id {
continue;
}
return Ok(Some(IcmpEchoReply {
ipid: ip.get_identification(),
ttl: ip.get_ttl(),
df: ip.get_flags() & 0b010 != 0,
code: p[1],
}));
}
}
fn recv_icmp_unreachable(
rx: &mut TransportReceiver,
target: Ipv4Addr,
expected_udp_dport: u16,
timeout: Duration,
) -> io::Result<Option<IcmpUnreachReply>> {
let deadline = Instant::now() + timeout;
loop {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Ok(None);
}
let wait = remaining.min(Duration::from_millis(50));
let Some(ip) = recv_ipv4_with_timeout(rx, wait)? else {
continue;
};
if ip.get_source() != target {
continue;
}
if ip.get_next_level_protocol() != IpNextHeaderProtocols::Icmp {
continue;
}
let p = ip.payload();
if p.len() < 36 {
continue;
} if p[0] != 3 || p[1] != 3 {
continue;
}
let embedded = &p[8..];
let ihl = (embedded[0] & 0x0f) as usize * 4;
if embedded.len() < ihl + 4 {
continue;
}
let udp_dport = u16::from_be_bytes([embedded[ihl + 2], embedded[ihl + 3]]);
if udp_dport != expected_udp_dport {
continue;
}
let unused = u32::from_be_bytes([p[4], p[5], p[6], p[7]]);
let eip_total = u16::from_be_bytes([embedded[2], embedded[3]]);
let eip_id = u16::from_be_bytes([embedded[4], embedded[5]]);
let eip_ck = u16::from_be_bytes([embedded[10], embedded[11]]);
let mut hdr_copy = embedded[..ihl].to_vec();
hdr_copy[10] = 0;
hdr_copy[11] = 0;
let eip_ck_computed = internet_checksum(&hdr_copy);
let eudp_ck = if embedded.len() >= ihl + 8 {
u16::from_be_bytes([embedded[ihl + 6], embedded[ihl + 7]])
} else {
0
};
let eudp_data = if embedded.len() > ihl + 8 {
embedded[ihl + 8..].to_vec()
} else {
Vec::new()
};
return Ok(Some(IcmpUnreachReply {
ttl: ip.get_ttl(),
df: ip.get_flags() & 0b010 != 0,
ip_total_len: ip.get_total_length(),
unused,
embedded_ip_total_len: eip_total,
embedded_ip_id: eip_id,
embedded_ip_cksum: eip_ck,
embedded_ip_cksum_computed: eip_ck_computed,
embedded_udp_cksum: eudp_ck,
embedded_udp_data: eudp_data,
}));
}
}
#[allow(clippy::too_many_arguments)] fn send_tcp_probe(
tx: &mut TransportSender,
src: Ipv4Addr,
dst: Ipv4Addr,
sport: u16,
dport: u16,
seq: u32,
ack: u32,
reserved: u8,
flags: u8,
window: u16,
urg: u16,
df: bool,
opts: &[u8],
ttl: u8,
ip_id: u16,
) -> io::Result<()> {
let tcp_len = 20 + opts.len();
let total = 20 + tcp_len;
let mut buf = vec![0u8; total];
{
let t = &mut buf[20..20 + tcp_len];
t[0..2].copy_from_slice(&sport.to_be_bytes());
t[2..4].copy_from_slice(&dport.to_be_bytes());
t[4..8].copy_from_slice(&seq.to_be_bytes());
t[8..12].copy_from_slice(&ack.to_be_bytes());
t[12] = (((tcp_len / 4) as u8) << 4) | (reserved & 0x0f);
t[13] = flags;
t[14..16].copy_from_slice(&window.to_be_bytes());
t[18..20].copy_from_slice(&urg.to_be_bytes());
t[20..].copy_from_slice(opts);
let pkt = TcpPacket::new(t).expect("tcp len");
let ck = ipv4_checksum(&pkt, &src, &dst);
t[16..18].copy_from_slice(&ck.to_be_bytes());
}
build_ip_header(&mut buf[..20], total as u16, ip_id, df, ttl, 6, src, dst);
tx.send_to(Ipv4Packet::new(&buf).expect("pkt"), IpAddr::V4(dst))?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn send_udp_probe(
tx: &mut TransportSender,
src: Ipv4Addr,
dst: Ipv4Addr,
sport: u16,
dport: u16,
data: &[u8],
ttl: u8,
df: bool,
ip_id: u16,
) -> io::Result<u16> {
let udp_len = 8 + data.len();
let total = 20 + udp_len;
let mut buf = vec![0u8; total];
{
let u = &mut buf[20..20 + udp_len];
u[0..2].copy_from_slice(&sport.to_be_bytes());
u[2..4].copy_from_slice(&dport.to_be_bytes());
u[4..6].copy_from_slice(&(udp_len as u16).to_be_bytes());
u[8..8 + data.len()].copy_from_slice(data);
let mut pseudo = Vec::with_capacity(12 + udp_len);
pseudo.extend_from_slice(&src.octets());
pseudo.extend_from_slice(&dst.octets());
pseudo.extend_from_slice(&[0, 17]);
pseudo.extend_from_slice(&(udp_len as u16).to_be_bytes());
pseudo.extend_from_slice(&u[..udp_len]);
let ck = internet_checksum(&pseudo);
u[6..8].copy_from_slice(&ck.to_be_bytes());
}
build_ip_header(&mut buf[..20], total as u16, ip_id, df, ttl, 17, src, dst);
let sent_ck = u16::from_be_bytes([buf[26], buf[27]]);
tx.send_to(Ipv4Packet::new(&buf).expect("pkt"), IpAddr::V4(dst))?;
Ok(sent_ck)
}
#[allow(clippy::too_many_arguments)]
fn send_icmp_echo(
tx: &mut TransportSender,
src: Ipv4Addr,
dst: Ipv4Addr,
code: u8,
id: u16,
seq: u16,
data_len: usize,
ttl: u8,
df: bool,
tos: u8,
ip_id: u16,
) -> io::Result<()> {
let icmp_len = 8 + data_len;
let total = 20 + icmp_len;
let mut buf = vec![0u8; total];
{
let ic = &mut buf[20..20 + icmp_len];
ic[0] = 8; ic[1] = code;
ic[4..6].copy_from_slice(&id.to_be_bytes());
ic[6..8].copy_from_slice(&seq.to_be_bytes());
let ck = internet_checksum(&ic[..icmp_len]);
ic[2..4].copy_from_slice(&ck.to_be_bytes());
}
{
let mut ip = MutableIpv4Packet::new(&mut buf[..20]).expect("ip");
ip.set_version(4);
ip.set_header_length(5);
ip.set_dscp(tos >> 2);
ip.set_ecn(tos & 0x03);
ip.set_total_length(total as u16);
ip.set_identification(ip_id);
if df {
ip.set_flags(Ipv4Flags::DontFragment);
}
ip.set_fragment_offset(0);
ip.set_ttl(ttl);
ip.set_next_level_protocol(IpNextHeaderProtocols::Icmp);
ip.set_checksum(0);
ip.set_source(src);
ip.set_destination(dst);
ip.set_checksum(ipv4_hdr_cksum(&ip.to_immutable()));
}
tx.send_to(Ipv4Packet::new(&buf).expect("pkt"), IpAddr::V4(dst))?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn build_ip_header(
buf: &mut [u8],
total_len: u16,
id: u16,
df: bool,
ttl: u8,
proto: u8,
src: Ipv4Addr,
dst: Ipv4Addr,
) {
let mut ip = MutableIpv4Packet::new(buf).expect("ip hdr");
ip.set_version(4);
ip.set_header_length(5);
ip.set_dscp(0);
ip.set_ecn(0);
ip.set_total_length(total_len);
ip.set_identification(id);
if df {
ip.set_flags(Ipv4Flags::DontFragment);
}
ip.set_fragment_offset(0);
ip.set_ttl(ttl);
ip.set_next_level_protocol(pnet::packet::ip::IpNextHeaderProtocol::new(proto));
ip.set_checksum(0);
ip.set_source(src);
ip.set_destination(dst);
ip.set_checksum(ipv4_hdr_cksum(&ip.to_immutable()));
}
fn classify_ipid(ipids: &[u16]) -> String {
if ipids.len() < 2 {
return "O".into();
}
if ipids.iter().all(|&v| v == 0) {
return "Z".into();
}
let mut diffs = Vec::with_capacity(ipids.len() - 1);
for i in 1..ipids.len() {
let cur = ipids[i] as u32;
let prev = ipids[i - 1] as u32;
let d = if cur >= prev {
cur - prev
} else {
0x10000 + cur - prev
};
diffs.push(d);
}
if diffs.iter().all(|&d| d == 0) {
return "Z".into();
}
if diffs.iter().any(|&d| d > 20000) {
return "RD".into();
}
let mut bi = true;
for i in 1..ipids.len() {
let prev = ipids[i - 1].swap_bytes() as u32;
let cur = ipids[i].swap_bytes() as u32;
let d = if cur >= prev {
cur - prev
} else {
0x10000 + cur - prev
};
if d > 20000 {
bi = false;
break;
}
}
if bi {
let mut swap_diffs = Vec::new();
for i in 1..ipids.len() {
let prev = ipids[i - 1].swap_bytes() as u32;
let cur = ipids[i].swap_bytes() as u32;
let d = if cur >= prev {
cur - prev
} else {
0x10000 + cur - prev
};
swap_diffs.push(d);
}
if swap_diffs.iter().all(|&d| d <= 20000) {
return "BI".into();
}
}
let avg: f64 = diffs.iter().map(|&d| d as f64).sum::<f64>() / diffs.len() as f64;
if avg < 1.0 {
return "Z".into();
}
let var: f64 = diffs
.iter()
.map(|&d| {
let x = d as f64 - avg;
x * x
})
.sum::<f64>()
/ diffs.len() as f64;
if var.sqrt() / avg < 0.1 && avg < 5000.0 {
return "I".into();
}
"RI".into()
}
fn classify_ts(timestamps: &[u32], times: &[Instant]) -> String {
if timestamps.len() < 2 {
return "U".into();
}
if timestamps.iter().all(|&t| t == 0) {
return "0".into();
}
for i in 1..timestamps.len() {
if timestamps[i] < timestamps[i - 1] && timestamps[i - 1] - timestamps[i] > 1000 {
return "U".into();
}
}
let ts_diff = timestamps.last().unwrap().wrapping_sub(timestamps[0]) as f64;
let time_diff = times.last().unwrap().duration_since(times[0]).as_secs_f64();
if time_diff < 0.001 {
return "U".into();
}
let freq = ts_diff / time_diff;
if freq < 0.5 {
return "0".into();
}
if freq < 5.66 {
return "1".into();
}
if (85.0..350.0).contains(&freq) {
return "7".into();
}
if (700.0..1500.0).contains(&freq) {
return "8".into();
}
format!("{:X}", freq.round() as u32)
}
fn shared_ipid_seq(tcp_ipids: &[u16], icmp_ipids: &[u16]) -> String {
if tcp_ipids.len() < 2 || icmp_ipids.is_empty() {
return "O".into();
}
let ti = classify_ipid(tcp_ipids);
let ii = classify_ipid(icmp_ipids);
if ti != "I" || ii != "I" {
return "O".into();
}
let last_tcp = *tcp_ipids.last().unwrap() as i32;
let first_icmp = icmp_ipids[0] as i32;
if (first_icmp - last_tcp).unsigned_abs() < 256 {
"S".into()
} else {
"O".into()
}
}
fn mod_diff_u32(a: u32, b: u32) -> u32 {
a.wrapping_sub(b).min(b.wrapping_sub(a))
}
fn gcd_many(vals: &[u32]) -> u32 {
if vals.is_empty() {
return 1;
}
vals.iter().copied().reduce(gcd_two).unwrap_or(1)
}
fn gcd_two(mut a: u32, mut b: u32) -> u32 {
while b != 0 {
let t = a % b;
a = b;
b = t;
}
a.max(1)
}
fn build_seq_test(
seq_nums: &[u32],
seq_times: &[Instant],
timestamps: &[u32],
tcp_open_ipids: &[u16],
tcp_closed_ipids: &[u16],
icmp_ipids: &[u16],
) -> HashMap<String, String> {
let n = seq_nums.len();
let mut diffs = Vec::with_capacity(n - 1);
for i in 1..n {
diffs.push(mod_diff_u32(seq_nums[i], seq_nums[i - 1]));
}
let gcd = gcd_many(&diffs);
let mut seq_rates = Vec::with_capacity(n - 1);
for i in 1..n {
let dt = seq_times[i]
.saturating_duration_since(seq_times[i - 1])
.as_micros()
.max(1);
seq_rates.push(diffs[i - 1] as f64 * 1_000_000.0 / dt as f64);
}
let avg: f64 = seq_rates.iter().sum::<f64>() / seq_rates.len().max(1) as f64;
let mut isr = if avg > 0.0 {
(avg.log2() * 8.0).round() as u32
} else {
0
};
if gcd == 0 {
isr = 0;
}
let div_gcd = if gcd > 9 { gcd as f64 } else { 1.0 };
let mean_normed = avg / div_gcd;
let var: f64 = seq_rates
.iter()
.map(|r| {
let t = r / div_gcd - mean_normed;
t * t
})
.sum::<f64>()
/ (seq_rates.len().saturating_sub(1).max(1)) as f64;
let sd = var.sqrt();
let sp = if sd <= 1.0 {
0u32
} else {
((sd.log2() * 8.0).round() as u32).min(0xff)
};
let mut m = HashMap::new();
m.insert("SP".into(), hex_val(sp));
m.insert("GCD".into(), hex_val(gcd));
m.insert("ISR".into(), hex_val(isr));
m.insert("TI".into(), classify_ipid(tcp_open_ipids));
m.insert(
"CI".into(),
if tcp_closed_ipids.is_empty() {
"Z".into()
} else {
classify_ipid(tcp_closed_ipids)
},
);
m.insert(
"II".into(),
if icmp_ipids.is_empty() {
"O".into()
} else {
classify_ipid(icmp_ipids)
},
);
m.insert("SS".into(), shared_ipid_seq(tcp_open_ipids, icmp_ipids));
m.insert("TS".into(), classify_ts(timestamps, seq_times));
m
}
fn build_ecn_test(reply: &Option<TcpReply>, probe_seq: u32) -> Option<HashMap<String, String>> {
let mut m = HashMap::new();
let Some(r) = reply else {
m.insert("R".into(), "N".into());
return Some(m);
};
m.insert("R".into(), "Y".into());
m.insert("DF".into(), if r.df { "Y" } else { "N" }.into());
m.insert("T".into(), hex_val(r.ttl as u32));
m.insert("TG".into(), hex_val(ttl_guess(r.ttl) as u32));
m.insert("W".into(), hex_val(r.window as u32));
m.insert("O".into(), r.options_fp.clone());
m.insert("CC".into(), cc_str(r.flags));
m.insert("Q".into(), quirks_str(r.reserved, r.flags, r.urg_ptr));
let _ = probe_seq;
Some(m)
}
fn build_t1_test(
reply: &Option<TcpReply>,
probe_seq: u32,
probe_ack: u32,
) -> Option<HashMap<String, String>> {
let mut m = HashMap::new();
let Some(r) = reply else {
m.insert("R".into(), "N".into());
return Some(m);
};
m.insert("R".into(), "Y".into());
m.insert("DF".into(), if r.df { "Y" } else { "N" }.into());
m.insert("T".into(), hex_val(r.ttl as u32));
m.insert("TG".into(), hex_val(ttl_guess(r.ttl) as u32));
m.insert("S".into(), seq_relation(r.seq, probe_ack));
m.insert("A".into(), ack_relation(r.ack_num, probe_seq));
m.insert("F".into(), flags_str(r.flags));
m.insert("RD".into(), rd_value(&r.data));
m.insert("Q".into(), quirks_str(r.reserved, r.flags, r.urg_ptr));
Some(m)
}
fn build_tx_test(
reply: &Option<TcpReply>,
probe_seq: u32,
probe_ack: u32,
) -> Option<HashMap<String, String>> {
let mut m = HashMap::new();
let Some(r) = reply else {
m.insert("R".into(), "N".into());
return Some(m);
};
m.insert("R".into(), "Y".into());
m.insert("DF".into(), if r.df { "Y" } else { "N" }.into());
m.insert("T".into(), hex_val(r.ttl as u32));
m.insert("TG".into(), hex_val(ttl_guess(r.ttl) as u32));
m.insert("W".into(), hex_val(r.window as u32));
m.insert("S".into(), seq_relation(r.seq, probe_ack));
m.insert("A".into(), ack_relation(r.ack_num, probe_seq));
m.insert("F".into(), flags_str(r.flags));
m.insert("O".into(), r.options_fp.clone());
m.insert("RD".into(), rd_value(&r.data));
m.insert("Q".into(), quirks_str(r.reserved, r.flags, r.urg_ptr));
Some(m)
}
fn build_u1_test(
reply: &Option<IcmpUnreachReply>,
sent_ip_id: u16,
sent_udp_cksum: u16,
sent_ip_total_len: u16,
sent_data: &[u8],
) -> Option<HashMap<String, String>> {
let mut m = HashMap::new();
let Some(r) = reply else {
m.insert("R".into(), "N".into());
return Some(m);
};
m.insert("R".into(), "Y".into());
m.insert("DF".into(), if r.df { "Y" } else { "N" }.into());
m.insert("T".into(), hex_val(r.ttl as u32));
m.insert("TG".into(), hex_val(ttl_guess(r.ttl) as u32));
m.insert("IPL".into(), hex_val(r.ip_total_len as u32));
m.insert("UN".into(), hex_val(r.unused));
m.insert(
"RIPL".into(),
if r.embedded_ip_total_len == sent_ip_total_len {
"G".into()
} else {
hex_val(r.embedded_ip_total_len as u32)
},
);
m.insert(
"RID".into(),
if r.embedded_ip_id == sent_ip_id {
"G".into()
} else {
hex_val(r.embedded_ip_id as u32)
},
);
m.insert(
"RIPCK".into(),
if r.embedded_ip_cksum == 0 {
"Z".into()
} else if r.embedded_ip_cksum == r.embedded_ip_cksum_computed {
"G".into()
} else {
"I".into()
},
);
m.insert(
"RUCK".into(),
if r.embedded_udp_cksum == 0 {
"Z".into()
} else if r.embedded_udp_cksum == sent_udp_cksum {
"G".into()
} else {
"I".into()
},
);
let rud_ok = r.embedded_udp_data.len() >= sent_data.len().min(r.embedded_udp_data.len())
&& r.embedded_udp_data
.starts_with(&sent_data[..sent_data.len().min(r.embedded_udp_data.len())]);
m.insert(
"RUD".into(),
if rud_ok && !r.embedded_udp_data.is_empty() {
"G".into()
} else {
"I".into()
},
);
Some(m)
}
fn build_ie_test(
ie1: &Option<IcmpEchoReply>,
ie2: &Option<IcmpEchoReply>,
) -> Option<HashMap<String, String>> {
let mut m = HashMap::new();
if ie1.is_none() && ie2.is_none() {
m.insert("R".into(), "N".into());
return Some(m);
}
m.insert("R".into(), "Y".into());
let dfi = match (ie1.as_ref().map(|r| r.df), ie2.as_ref().map(|r| r.df)) {
(Some(true), Some(true)) => "Y",
(Some(false), Some(false)) => "N",
(Some(true), Some(false)) => "S",
_ => "O",
};
m.insert("DFI".into(), dfi.into());
if let Some(r) = ie1.as_ref().or(ie2.as_ref()) {
m.insert("T".into(), hex_val(r.ttl as u32));
m.insert("TG".into(), hex_val(ttl_guess(r.ttl) as u32));
}
let cd = match (ie1.as_ref().map(|r| r.code), ie2.as_ref().map(|r| r.code)) {
(Some(0), Some(0)) => "Z",
(Some(9), Some(0)) => "S",
_ => "O",
};
m.insert("CD".into(), cd.into());
Some(m)
}
pub fn probe_ipv4_os(
target: Ipv4Addr,
open_tcp: u16,
closed_tcp: u16,
closed_udp: u16,
probe_timeout: Duration,
) -> Result<SubjectFingerprint> {
let (mut tcp_tx, mut tcp_rx) = transport_channel(
65536,
TransportChannelType::Layer3(IpNextHeaderProtocols::Tcp),
)
.context("raw TCP for OS detection — try sudo / capabilities")?;
let (_icmp_tx, mut icmp_rx) = transport_channel(
65536,
TransportChannelType::Layer3(IpNextHeaderProtocols::Icmp),
)
.context("raw ICMP for OS detection")?;
let src = local_ipv4().context("local IPv4 for OS probes")?;
let mut rng = rand::thread_rng();
let tcp_port_base: u16 = 33000 + (rng.gen::<u32>() % PRIME_32K) as u16;
let udp_port_base: u16 = 33000 + (rng.gen::<u32>() % PRIME_32K) as u16;
let tcp_seq_base: u32 = rng.gen();
let tcp_ack: u32 = rng.gen();
let icmp_id: u16 = rng.gen();
let mut ipid_ctr: u16 = rng.gen();
let mut seq_replies: [Option<TcpReply>; NUM_SEQ_SAMPLES] = std::array::from_fn(|_| None);
for i in 0..NUM_SEQ_SAMPLES {
let sport = tcp_port_base + i as u16;
let id = next_ipid(&mut ipid_ctr);
send_tcp_probe(
&mut tcp_tx,
src,
target,
sport,
open_tcp,
tcp_seq_base + i as u32,
0,
0,
TcpFlags::SYN,
PRB_WIN[i],
0,
false,
PRB_OPTS[i],
64,
id,
)?;
seq_replies[i] = recv_tcp_reply(&mut tcp_rx, target, sport, probe_timeout)?;
if i + 1 < NUM_SEQ_SAMPLES {
thread::sleep(Duration::from_millis(OS_SEQ_PROBE_DELAY_MS));
}
}
let ecn_sport = tcp_port_base + 6;
send_tcp_probe(
&mut tcp_tx,
src,
target,
ecn_sport,
open_tcp,
tcp_seq_base,
0,
0,
TcpFlags::SYN | TcpFlags::ECE | TcpFlags::CWR,
PRB_WIN[6],
0,
false,
PRB_OPTS[6],
64,
next_ipid(&mut ipid_ctr),
)?;
let ecn_reply = recv_tcp_reply(&mut tcp_rx, target, ecn_sport, probe_timeout)?;
let mut send_t = |idx: usize, dport: u16, flags: u8| -> io::Result<Option<TcpReply>> {
let sport = tcp_port_base + idx as u16;
let id = next_ipid(&mut ipid_ctr);
send_tcp_probe(
&mut tcp_tx,
src,
target,
sport,
dport,
tcp_seq_base,
tcp_ack,
0,
flags,
PRB_WIN[idx],
0,
false,
PRB_OPTS[idx],
64,
id,
)?;
recv_tcp_reply(&mut tcp_rx, target, sport, probe_timeout)
};
let t2_reply = send_t(7, open_tcp, 0)?;
let t3_reply = send_t(
8,
open_tcp,
TcpFlags::SYN | TcpFlags::FIN | TcpFlags::URG | TcpFlags::PSH,
)?;
let t4_reply = send_t(9, open_tcp, TcpFlags::ACK)?;
let t5_reply = send_t(10, closed_tcp, TcpFlags::SYN)?;
let t6_reply = send_t(11, closed_tcp, TcpFlags::ACK)?;
let t7_reply = send_t(
12,
closed_tcp,
TcpFlags::FIN | TcpFlags::PSH | TcpFlags::URG,
)?;
let u1_data = [0x43u8; 300];
let u1_ipid = next_ipid(&mut ipid_ctr);
let u1_ip_total: u16 = 20 + 8 + 300;
let u1_udp_ck = send_udp_probe(
&mut tcp_tx,
src,
target,
udp_port_base,
closed_udp,
&u1_data,
64,
true,
u1_ipid,
)?;
let u1_reply = recv_icmp_unreachable(&mut icmp_rx, target, closed_udp, probe_timeout)?;
send_icmp_echo(
&mut tcp_tx,
src,
target,
9,
icmp_id,
295,
120,
64,
true,
0,
next_ipid(&mut ipid_ctr),
)?;
let ie1 = recv_icmp_echo_reply(&mut icmp_rx, target, icmp_id, probe_timeout)?;
thread::sleep(Duration::from_millis(OS_SEQ_PROBE_DELAY_MS));
send_icmp_echo(
&mut tcp_tx,
src,
target,
0,
icmp_id + 1,
296,
150,
64,
false,
4,
next_ipid(&mut ipid_ctr),
)?;
let ie2 = recv_icmp_echo_reply(&mut icmp_rx, target, icmp_id + 1, probe_timeout)?;
let mut subject = SubjectFingerprint::default();
let good: Vec<usize> = (0..NUM_SEQ_SAMPLES)
.filter(|&i| seq_replies[i].is_some())
.collect();
if good.len() >= 4 {
let tcp_open_ipids: Vec<u16> = good
.iter()
.map(|&i| seq_replies[i].as_ref().unwrap().ipid)
.collect();
let tcp_closed_ipids: Vec<u16> = [&t5_reply, &t6_reply, &t7_reply]
.iter()
.filter_map(|r| r.as_ref().map(|t| t.ipid))
.collect();
let icmp_ipids: Vec<u16> = [&ie1, &ie2]
.iter()
.filter_map(|r| r.as_ref().map(|e| e.ipid))
.collect();
let seq_nums: Vec<u32> = good
.iter()
.map(|&i| seq_replies[i].as_ref().unwrap().seq)
.collect();
let seq_times: Vec<Instant> = good
.iter()
.map(|&i| seq_replies[i].as_ref().unwrap().recv_time)
.collect();
let timestamps: Vec<u32> = good
.iter()
.map(|&i| seq_replies[i].as_ref().unwrap().tsval)
.collect();
subject.tests[0] = Some(build_seq_test(
&seq_nums,
&seq_times,
×tamps,
&tcp_open_ipids,
&tcp_closed_ipids,
&icmp_ipids,
));
let mut ops = HashMap::new();
for (j, &i) in good.iter().enumerate().take(6) {
ops.insert(
format!("O{}", j + 1),
seq_replies[i].as_ref().unwrap().options_fp.clone(),
);
}
subject.tests[1] = Some(ops);
let mut win = HashMap::new();
for (j, &i) in good.iter().enumerate().take(6) {
win.insert(
format!("W{}", j + 1),
hex_val(seq_replies[i].as_ref().unwrap().window as u32),
);
}
subject.tests[2] = Some(win);
}
subject.tests[3] = build_ecn_test(&ecn_reply, tcp_seq_base);
subject.tests[4] = build_t1_test(&seq_replies[0], tcp_seq_base, 0);
subject.tests[5] = build_tx_test(&t2_reply, tcp_seq_base, tcp_ack);
subject.tests[6] = build_tx_test(&t3_reply, tcp_seq_base, tcp_ack);
subject.tests[7] = build_tx_test(&t4_reply, tcp_seq_base, tcp_ack);
subject.tests[8] = build_tx_test(&t5_reply, tcp_seq_base, tcp_ack);
subject.tests[9] = build_tx_test(&t6_reply, tcp_seq_base, tcp_ack);
subject.tests[10] = build_tx_test(&t7_reply, tcp_seq_base, tcp_ack);
subject.tests[11] = build_u1_test(&u1_reply, u1_ipid, u1_udp_ck, u1_ip_total, &u1_data);
subject.tests[12] = build_ie_test(&ie1, &ie2);
Ok(subject)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn internet_checksum_known_vector_rfc1071() {
let data = [0x00, 0x01, 0xf2, 0x03, 0xf4, 0xf5, 0xf6, 0xf7];
assert_eq!(internet_checksum(&data), 0x220d);
}
#[test]
fn internet_checksum_self_inverse() {
let mut data = vec![
0x45, 0x00, 0x00, 0x14, 0x00, 0x00, 0x40, 0x00, 0x40, 0x06, 0x00,
0x00, 192, 168, 0, 1, 192, 168, 0, 2,
];
let cs = internet_checksum(&data);
data[10] = (cs >> 8) as u8;
data[11] = cs as u8;
assert_eq!(
internet_checksum(&data),
0,
"packet with embedded checksum must verify as 0"
);
}
#[test]
fn internet_checksum_handles_odd_length() {
let data = [0xaa, 0xbb, 0xcc];
assert_eq!(internet_checksum(&data), 0x8943);
}
#[test]
fn internet_checksum_empty_returns_all_ones() {
assert_eq!(internet_checksum(&[]), 0xffff);
}
#[test]
fn ttl_guess_buckets() {
assert_eq!(ttl_guess(1), 32);
assert_eq!(ttl_guess(32), 32);
assert_eq!(ttl_guess(33), 64);
assert_eq!(ttl_guess(64), 64);
assert_eq!(ttl_guess(65), 128);
assert_eq!(ttl_guess(128), 128);
assert_eq!(ttl_guess(129), 255);
assert_eq!(ttl_guess(255), 255);
}
#[test]
fn flags_str_canonical_order_for_syn_ack() {
let s = flags_str(TcpFlags::SYN | TcpFlags::ACK);
assert_eq!(s, "AS");
}
#[test]
fn flags_str_empty_when_no_flags() {
assert_eq!(flags_str(0), "");
}
#[test]
fn flags_str_full_house() {
let all = TcpFlags::ECE
| TcpFlags::URG
| TcpFlags::ACK
| TcpFlags::PSH
| TcpFlags::RST
| TcpFlags::SYN
| TcpFlags::FIN;
assert_eq!(flags_str(all), "EUAPRSF");
}
#[test]
fn quirks_reserved_bits_set() {
assert_eq!(quirks_str(1, 0, 0), "R");
}
#[test]
fn quirks_urg_pointer_set_without_urg_flag() {
assert_eq!(quirks_str(0, 0, 1234), "U");
}
#[test]
fn quirks_urg_pointer_with_urg_flag_no_quirk() {
assert_eq!(quirks_str(0, TcpFlags::URG, 1234), "");
}
#[test]
fn quirks_both_reserved_and_urg() {
assert_eq!(quirks_str(1, 0, 100), "RU");
}
#[test]
fn seq_relation_zero_when_seq_is_zero() {
assert_eq!(seq_relation(0, 12345), "Z");
}
#[test]
fn seq_relation_equals_probe_ack_is_a() {
assert_eq!(seq_relation(12345, 12345), "A");
}
#[test]
fn seq_relation_one_above_probe_ack_is_a_plus() {
assert_eq!(seq_relation(12346, 12345), "A+");
}
#[test]
fn seq_relation_other_when_unrelated() {
assert_eq!(seq_relation(99999, 12345), "O");
}
#[test]
fn seq_relation_zero_branch_wins_over_wrapping() {
assert_eq!(seq_relation(0, u32::MAX), "Z");
}
#[test]
fn seq_relation_wrapping_a_plus_when_seq_nonzero() {
assert_eq!(seq_relation(u32::MAX, u32::MAX - 1), "A+");
}
#[test]
fn ack_relation_zero() {
assert_eq!(ack_relation(0, 100), "Z");
}
#[test]
fn ack_relation_matches_probe_seq() {
assert_eq!(ack_relation(100, 100), "S");
}
#[test]
fn ack_relation_one_above_is_s_plus() {
assert_eq!(ack_relation(101, 100), "S+");
}
#[test]
fn ack_relation_other() {
assert_eq!(ack_relation(50000, 100), "O");
}
#[test]
fn cc_ece_only_is_y() {
assert_eq!(cc_str(TcpFlags::ECE), "Y");
}
#[test]
fn cc_neither_is_n() {
assert_eq!(cc_str(0), "N");
}
#[test]
fn cc_both_is_s() {
assert_eq!(cc_str(TcpFlags::ECE | TcpFlags::CWR), "S");
}
#[test]
fn cc_cwr_only_is_o() {
assert_eq!(cc_str(TcpFlags::CWR), "O");
}
#[test]
fn rd_value_empty_data_is_zero_literal() {
assert_eq!(rd_value(&[]), "0");
}
#[test]
fn rd_value_deterministic_for_same_input() {
let a = rd_value(b"hello");
let b = rd_value(b"hello");
assert_eq!(a, b);
}
#[test]
fn rd_value_diverges_on_one_byte_mutation() {
assert_ne!(rd_value(b"hello"), rd_value(b"hellp"));
}
#[test]
fn next_ipid_returns_current_then_increments() {
let mut c: u16 = 42;
assert_eq!(next_ipid(&mut c), 42);
assert_eq!(c, 43);
}
#[test]
fn next_ipid_wraps_at_max() {
let mut c: u16 = u16::MAX;
assert_eq!(next_ipid(&mut c), u16::MAX);
assert_eq!(c, 0, "wraps cleanly to 0");
}
#[test]
fn gcd_two_basic() {
assert_eq!(gcd_two(48, 18), 6);
assert_eq!(gcd_two(100, 25), 25);
assert_eq!(gcd_two(17, 13), 1); }
#[test]
fn gcd_two_with_zero_returns_other_min_1() {
assert_eq!(gcd_two(0, 0), 1);
assert_eq!(gcd_two(7, 0), 7);
assert_eq!(gcd_two(0, 5), 5);
}
#[test]
fn gcd_many_empty_returns_one() {
assert_eq!(gcd_many(&[]), 1);
}
#[test]
fn gcd_many_single_value_returns_self() {
assert_eq!(gcd_many(&[42]), 42);
}
#[test]
fn gcd_many_reduces_across_values() {
assert_eq!(gcd_many(&[12, 18, 30]), 6);
}
#[test]
fn gcd_many_coprime_set_is_one() {
assert_eq!(gcd_many(&[7, 11, 13]), 1);
}
#[test]
fn mod_diff_picks_shorter_arc() {
assert_eq!(mod_diff_u32(10, 5), 5);
assert_eq!(mod_diff_u32(5, 10), 5);
}
#[test]
fn mod_diff_wraps_around_u32_max() {
assert_eq!(mod_diff_u32(u32::MAX, 0), 1);
assert_eq!(mod_diff_u32(0, u32::MAX), 1);
}
#[test]
fn mod_diff_self_is_zero() {
assert_eq!(mod_diff_u32(12345, 12345), 0);
}
#[test]
fn classify_ipid_under_two_samples_is_other() {
assert_eq!(classify_ipid(&[]), "O");
assert_eq!(classify_ipid(&[100]), "O");
}
#[test]
fn classify_ipid_all_zero_is_z() {
assert_eq!(classify_ipid(&[0, 0, 0, 0]), "Z");
}
#[test]
fn classify_ipid_zero_diffs_is_z() {
assert_eq!(classify_ipid(&[42, 42, 42]), "Z");
}
#[test]
fn classify_ipid_random_jumps_is_rd() {
assert_eq!(classify_ipid(&[100, 60_000, 5_000]), "RD");
}
#[test]
fn classify_ipid_small_byte_swapped_increments_is_bi() {
assert_eq!(
classify_ipid(&[100, 110, 120, 130, 140]),
"BI",
"byte-swap-orderly sequence MUST trip BI before I"
);
}
#[test]
fn classify_ipid_incremental_when_byte_swap_path_disqualified() {
assert_eq!(classify_ipid(&[100, 200, 300]), "I");
}
#[test]
fn hex_val_uppercase_no_prefix() {
assert_eq!(hex_val(0xdeadbeef), "DEADBEEF");
assert_eq!(hex_val(0), "0");
assert_eq!(hex_val(0xff), "FF");
}
#[test]
fn seq_relation_one_below_probe_ack_is_other() {
assert_eq!(seq_relation(12344, 12345), "O");
}
#[test]
fn ack_relation_one_below_probe_seq_is_other() {
assert_eq!(ack_relation(99, 100), "O");
}
#[test]
fn classify_ipid_two_samples_small_diff_is_bi_or_incremental() {
let label = classify_ipid(&[1000, 1001]);
assert!(label == "BI" || label == "I", "unexpected {label}");
}
#[test]
fn gcd_many_all_equal_values() {
assert_eq!(gcd_many(&[7, 7, 7, 7]), 7);
}
#[test]
fn mod_diff_half_u32_range() {
assert_eq!(mod_diff_u32(0, 1u32 << 31), 1u32 << 31);
}
#[test]
fn rd_value_differs_for_empty_vs_nonempty() {
assert_ne!(rd_value(&[]), rd_value(b"x"));
}
#[test]
fn quirks_clean_packet_has_no_quirks() {
assert_eq!(quirks_str(0, 0, 0), "");
}
#[test]
fn classify_ts_single_sample_is_u() {
assert_eq!(classify_ts(&[1000], &[Instant::now()]), "U");
}
#[test]
fn classify_ts_all_zero_tsvals_is_zero() {
let t0 = Instant::now();
let t1 = t0 + Duration::from_millis(500);
assert_eq!(classify_ts(&[0, 0, 0], &[t0, t1, t1]), "0");
}
#[test]
fn classify_ts_same_recv_times_is_u() {
let t0 = Instant::now();
assert_eq!(classify_ts(&[1000, 5000], &[t0, t0]), "U");
}
#[test]
fn shared_ipid_seq_needs_two_tcp_samples() {
assert_eq!(shared_ipid_seq(&[100], &[100, 101]), "O");
}
#[test]
fn shared_ipid_seq_needs_icmp_samples() {
assert_eq!(shared_ipid_seq(&[100, 200, 300], &[]), "O");
}
#[test]
fn shared_ipid_seq_incremental_tcp_and_icmp_close_is_s() {
assert_eq!(shared_ipid_seq(&[100, 200, 300], &[301, 401]), "S");
}
#[test]
fn shared_ipid_seq_far_icmp_gap_is_other() {
assert_eq!(shared_ipid_seq(&[100, 200, 300], &[1000, 1100]), "O");
}
#[test]
fn flags_str_fin_only() {
assert_eq!(flags_str(TcpFlags::FIN), "F");
}
#[test]
fn flags_str_rst_only() {
assert_eq!(flags_str(TcpFlags::RST), "R");
}
#[test]
fn flags_str_psh_urg_combo() {
assert_eq!(flags_str(TcpFlags::PSH | TcpFlags::URG), "UP");
}
#[test]
fn cc_str_cwr_without_ece_is_other() {
assert_eq!(cc_str(TcpFlags::CWR), "O");
}
#[test]
fn cc_str_ece_and_cwr_is_s() {
assert_eq!(cc_str(TcpFlags::ECE | TcpFlags::CWR), "S");
}
#[test]
fn rd_value_nonempty_is_uppercase_hex() {
let v = rd_value(b"probe");
assert!(!v.is_empty());
assert!(v.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn gcd_many_single_element() {
assert_eq!(gcd_many(&[42]), 42);
}
#[test]
fn mod_diff_u32_identical_is_zero() {
assert_eq!(mod_diff_u32(12345, 12345), 0);
}
#[test]
fn hex_val_single_byte() {
assert_eq!(hex_val(0xff), "FF");
}
#[test]
fn quirks_reserved_nonzero_is_r() {
assert_eq!(quirks_str(1, 0, 0), "R");
}
#[test]
fn quirks_urg_without_urg_flag_is_u() {
assert_eq!(quirks_str(0, 0, 1), "U");
}
#[test]
fn classify_ts_two_zeros_with_elapsed_time_is_zero() {
let t0 = Instant::now();
let t1 = t0 + Duration::from_secs(2);
assert_eq!(classify_ts(&[0, 0], &[t0, t1]), "0");
}
#[test]
fn classify_ts_slow_increment_returns_one_bucket() {
let t0 = Instant::now();
let t1 = t0 + Duration::from_secs(1);
assert_eq!(classify_ts(&[1000, 1004], &[t0, t1]), "1");
}
#[test]
fn ttl_guess_exactly_thirty_two() {
assert_eq!(ttl_guess(32), 32);
}
#[test]
fn internet_checksum_all_ones_word_folds_to_zero() {
let data = [0xff, 0xff];
assert_eq!(internet_checksum(&data), 0);
}
#[test]
fn internet_checksum_empty_is_ffff() {
assert_eq!(internet_checksum(&[]), !0u16);
}
#[test]
fn internet_checksum_odd_length_last_byte() {
let data = [0x00, 0x01, 0x02];
let cs = internet_checksum(&data);
assert_ne!(cs, 0);
}
#[test]
fn next_ipid_sequential_three_values() {
let mut c = 100u16;
assert_eq!(next_ipid(&mut c), 100);
assert_eq!(next_ipid(&mut c), 101);
assert_eq!(next_ipid(&mut c), 102);
}
#[test]
fn flags_str_syn_ack_combo() {
assert_eq!(flags_str(TcpFlags::SYN | TcpFlags::ACK), "AS");
}
#[test]
fn ttl_guess_one_returns_thirty_two() {
assert_eq!(ttl_guess(1), 32);
}
#[test]
fn ttl_guess_two_fifty_five_returns_two_fifty_five() {
assert_eq!(ttl_guess(255), 255);
}
#[test]
fn gcd_two_equal_nonzero() {
assert_eq!(gcd_two(13, 13), 13);
}
#[test]
fn mod_diff_u32_wraparound() {
assert_eq!(mod_diff_u32(0, u32::MAX), 1);
}
#[test]
fn classify_ipid_single_increment_is_i_or_bi() {
let label = classify_ipid(&[500, 501, 502, 503, 504]);
assert!(label == "I" || label == "BI", "unexpected {label}");
}
}