liarsping 0.1.0

A ping server which attempts to manipulate the ping times seen by the client
Documentation
use std::collections::HashMap;
use std::net::Ipv4Addr;
use std::time::{Duration, Instant};

#[derive(Debug, Clone, Copy)]
pub struct SenderState {
    pub last_seq: u16,
    pub last_seen: Instant,
}

#[derive(Default)]
pub struct SenderTable {
    inner: HashMap<(Ipv4Addr, u16), SenderState>,
}

/// If the new sequence is at least this much *less* than the last sequence,
/// treat it as a sender restart rather than a packet reorder.
const RESET_THRESHOLD: u16 = 100;

impl SenderTable {
    pub fn new() -> Self { Self::default() }

    /// Record a sighting. Returns `true` if this is a first sighting for the
    /// key, or if we detect a sequence reset (treated as fresh sender).
    pub fn touch(&mut self, key: (Ipv4Addr, u16), seq: u16, now: Instant) -> bool {
        match self.inner.get_mut(&key) {
            None => {
                self.inner.insert(key, SenderState { last_seq: seq, last_seen: now });
                true
            }
            Some(s) => {
                let reset = seq < s.last_seq && (s.last_seq - seq) >= RESET_THRESHOLD;
                s.last_seq = seq;
                s.last_seen = now;
                reset
            }
        }
    }

    pub fn evict_idle(&mut self, older_than: Duration, now: Instant) {
        self.inner.retain(|_, s| now.duration_since(s.last_seen) < older_than);
    }

    pub fn len(&self) -> usize { self.inner.len() }

    pub fn is_empty(&self) -> bool { self.inner.is_empty() }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn key() -> (Ipv4Addr, u16) { (Ipv4Addr::new(10, 0, 0, 1), 0x1234) }

    #[test]
    fn first_touch_is_first() {
        let mut t = SenderTable::new();
        assert!(t.touch(key(), 1, Instant::now()));
    }

    #[test]
    fn second_touch_is_not_first() {
        let mut t = SenderTable::new();
        let now = Instant::now();
        t.touch(key(), 1, now);
        assert!(!t.touch(key(), 2, now + Duration::from_millis(100)));
    }

    #[test]
    fn sequence_reset_counts_as_first() {
        let mut t = SenderTable::new();
        let now = Instant::now();
        t.touch(key(), 5_000, now);
        // big jump backwards -> treat as restart
        assert!(t.touch(key(), 1, now + Duration::from_secs(1)));
    }

    #[test]
    fn small_backward_jump_is_not_reset() {
        let mut t = SenderTable::new();
        let now = Instant::now();
        t.touch(key(), 50, now);
        // small backward (reorder), within RESET_THRESHOLD
        assert!(!t.touch(key(), 49, now + Duration::from_millis(10)));
    }

    #[test]
    fn evict_idle_drops_old_entries() {
        let mut t = SenderTable::new();
        let now = Instant::now();
        t.touch(key(), 1, now);
        t.evict_idle(Duration::from_secs(60), now + Duration::from_secs(120));
        assert_eq!(t.len(), 0);
    }

    #[test]
    fn evict_idle_keeps_recent_entries() {
        let mut t = SenderTable::new();
        let now = Instant::now();
        t.touch(key(), 1, now);
        t.evict_idle(Duration::from_secs(60), now + Duration::from_secs(10));
        assert_eq!(t.len(), 1);
    }
}