use crate::config::{Config, FirstPacketPolicy, SignedDuration, TimestampLie};
use crate::detect::{classify, Strategy};
use crate::payload;
use crate::preflight;
use crate::scheduler::{PendingReply, Scheduler};
use crate::state::SenderTable;
use icmp_socket::{IcmpSocket, IcmpSocket4, Icmpv4Message, Icmpv4Packet};
use std::net::Ipv4Addr;
use std::time::{Duration, Instant, SystemTime};
const IDLE_EVICT_INTERVAL: Duration = Duration::from_secs(10);
const IDLE_EVICT_AFTER: Duration = Duration::from_secs(60);
const DEFAULT_RECV_TIMEOUT: Duration = Duration::from_secs(1);
const MAX_PENDING_REPLIES: usize = 10_000;
pub fn run(config: Config) -> std::io::Result<()> {
preflight::check_and_warn();
let mut socket = IcmpSocket4::try_from(config.bind).map_err(|e| {
eprintln!("failed to bind ICMP socket on {}: {}. Try sudo or `setcap cap_net_raw+ep`.", config.bind, e);
e
})?;
let mut state = SenderTable::new();
let mut scheduler = Scheduler::new();
let mut last_evict = Instant::now();
loop {
let timeout = scheduler
.next_deadline()
.map(|d| d.saturating_duration_since(Instant::now()))
.unwrap_or(DEFAULT_RECV_TIMEOUT)
.max(Duration::from_millis(1));
socket.set_timeout(Some(timeout));
match socket.rcv_from() {
Ok((pkt, sock_addr)) => {
if let Some(src_v4) = sock_addr.as_socket_ipv4().map(|s| *s.ip()) {
handle_packet(&pkt, src_v4, &config, &mut state, &mut scheduler, &mut socket);
}
}
Err(_) => {
}
}
for reply in scheduler.pop_ready(Instant::now()) {
send_reply(&mut socket, reply.dest, reply.identifier, reply.sequence, reply.payload, config.verbose);
}
if last_evict.elapsed() >= IDLE_EVICT_INTERVAL {
state.evict_idle(IDLE_EVICT_AFTER, Instant::now());
last_evict = Instant::now();
}
}
}
fn handle_packet(
pkt: &Icmpv4Packet,
src: Ipv4Addr,
config: &Config,
state: &mut SenderTable,
scheduler: &mut Scheduler,
socket: &mut IcmpSocket4,
) {
let (identifier, sequence, payload) = match &pkt.message {
Icmpv4Message::Echo { identifier, sequence, payload } => (*identifier, *sequence, payload.clone()),
_ => return,
};
let mut mode = config
.forced_strategy
.unwrap_or_else(|| classify(&payload, SystemTime::now()));
if mode == Strategy::Timestamp
&& matches!(config.timestamp_lie, TimestampLie::Percent(_))
&& payload.len() < 12
{
mode = Strategy::Sequence;
}
let key = (src, identifier);
let first = state.touch(key, sequence, Instant::now());
if config.verbose {
eprintln!("recv from={src} id={identifier} seq={sequence} mode={mode:?} first={first}");
}
match mode {
Strategy::Timestamp => {
let shave = match config.timestamp_lie {
TimestampLie::Absolute(s) => s,
TimestampLie::Percent(p) => {
let t = payload::read_timestamp(&payload)
.expect("pre-classify guarantees payload >= 12 bytes");
let now = SystemTime::now();
let delta_us: i128 = match now.duration_since(t) {
Ok(d) => d.as_micros() as i128,
Err(e) => -(e.duration().as_micros() as i128),
};
let shave_us = ((delta_us as f64) * p / 100.0) as i128;
SignedDuration::from_micros_signed(shave_us)
}
};
let new_payload = payload::shave_signed(&payload, shave);
send_reply(socket, src, identifier, sequence, new_payload, config.verbose);
}
Strategy::Sequence => {
if first {
match config.first_packet {
FirstPacketPolicy::Drop => {
if config.verbose { eprintln!("drop first sequence-mode packet"); }
}
FirstPacketPolicy::Honest => {
send_reply(socket, src, identifier, sequence, payload, config.verbose);
}
}
return;
}
if scheduler.len() >= MAX_PENDING_REPLIES {
if config.verbose {
eprintln!("scheduler full ({} entries), dropping sequence-mode reply from {}", scheduler.len(), src);
}
return;
}
if config.sequence_shave.negative {
scheduler.push(PendingReply {
deadline: Instant::now() + config.sequence_shave.magnitude,
dest: src,
identifier,
sequence,
payload,
});
} else {
let shave = config.sequence_shave.magnitude;
let interval_us = config.assumed_interval.as_micros().max(1);
let shave_us = shave.as_micros();
let lookahead = shave_us.div_ceil(interval_us).max(1) as u16;
let total_advance = Duration::from_micros((lookahead as u64) * (interval_us as u64));
let delay = total_advance.checked_sub(shave).unwrap_or(Duration::ZERO);
scheduler.push(PendingReply {
deadline: Instant::now() + delay,
dest: src,
identifier,
sequence: sequence.wrapping_add(lookahead),
payload,
});
}
}
}
}
fn send_reply(
socket: &mut IcmpSocket4,
dest: Ipv4Addr,
identifier: u16,
sequence: u16,
payload: Vec<u8>,
verbose: bool,
) {
let pkt = Icmpv4Packet {
typ: 0,
code: 0,
checksum: 0,
message: Icmpv4Message::EchoReply { identifier, sequence, payload },
};
if let Err(e) = socket.send_to(dest, pkt) {
eprintln!("send failure to {}: {e}", dest);
} else if verbose {
eprintln!("send to={dest} id={identifier} seq={sequence}");
}
}