liarsping 0.1.0

A ping server which attempts to manipulate the ping times seen by the client
Documentation
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(_) => {
                // timeout or transient: fall through to drain
            }
        }

        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()));

    // Percent-mode cannot operate on payloads that don't carry an embedded
    // timestamp. Under `--strategy auto` the classifier already routes those
    // to Sequence; under forced `--strategy timestamp` we need to reclassify
    // for this single packet.
    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}");
    }
}