stochastic-routing-extended 1.0.2

SRX (Stochastic Routing eXtended) — a next-generation VPN protocol with stochastic routing, DPI evasion, post-quantum cryptography, and multi-transport channel splitting
Documentation
//! Self-healing: automatic recovery from DPI interference.
//!
//! When the client detects active interference (transport blockage), it:
//! 1. Updates the seed (forcing new stochastic decisions)
//! 2. Switches transports based on health scores
//! 3. Adjusts traffic profiles
//!
//! All without dropping the logical session.

use std::time::{Duration, Instant};

use crate::client::policy::TransportPolicy;
use crate::seed::SeedRng;
use crate::transport::{TransportKind, TransportManager};

/// Minimum backoff between healing attempts.
const MIN_BACKOFF: Duration = Duration::from_secs(1);
/// Maximum backoff between healing attempts.
const MAX_BACKOFF: Duration = Duration::from_secs(60);

/// Self-healing controller that responds to detected interference.
pub struct SelfHealing {
    /// Number of healing events triggered.
    pub heal_count: u32,
    /// Last time a heal was performed.
    last_heal: Option<Instant>,
    /// Current backoff duration (doubles after each heal, resets on success).
    backoff: Duration,
}

impl SelfHealing {
    pub fn new() -> Self {
        Self {
            heal_count: 0,
            last_heal: None,
            backoff: MIN_BACKOFF,
        }
    }

    /// Check whether healing should be triggered based on transport health.
    ///
    /// Returns `true` if any registered transport is blocked AND the backoff
    /// window has elapsed since the last heal.
    pub fn should_heal(&self, mgr: &TransportManager) -> bool {
        let any_blocked = mgr.active_kinds().iter().any(|&k| mgr.is_blocked(k));
        if !any_blocked {
            return false;
        }
        // Respect exponential backoff.
        match self.last_heal {
            Some(t) => t.elapsed() >= self.backoff,
            None => true,
        }
    }

    /// Reseeds [`SeedRng`] and returns transports ordered by health score.
    ///
    /// Healthy (non-blocked) transports come first, sorted by descending
    /// score. Blocked transports are appended at the end.
    pub fn heal(
        &mut self,
        rng: &mut SeedRng,
        policy: &TransportPolicy,
        mgr: &TransportManager,
    ) -> Vec<TransportKind> {
        self.heal_count += 1;
        self.last_heal = Some(Instant::now());
        // Exponential backoff, capped.
        self.backoff = (self.backoff * 2).min(MAX_BACKOFF);

        // Reseed (deterministic mix with heal_count).
        let mut s = rng.seed_bytes();
        s[0] ^= (self.heal_count as u8).wrapping_mul(0x9e);
        s[1] ^= ((self.heal_count >> 8) & 0xff) as u8;
        s[2] ^= ((self.heal_count >> 16) & 0xff) as u8;
        rng.reseed(s);

        // Prefer healthy transports, then fall back to policy order for blocked ones.
        let healthy = mgr.healthy_kinds();
        let all = mgr.active_kinds();
        let blocked: Vec<_> = all
            .iter()
            .filter(|k| !healthy.contains(k))
            .copied()
            .collect();

        // Policy-sort within each group.
        let mut result = policy.recommend(&healthy);
        let blocked_sorted = policy.recommend(&blocked);
        result.extend(blocked_sorted);
        result
    }

    /// Reseed the RNG and bump heal counters (without reading TransportManager).
    ///
    /// This is useful when the caller has already extracted health data and
    /// only needs the reseed + backoff update step.
    pub fn reseed_only(&mut self, rng: &mut SeedRng) {
        self.heal_count += 1;
        self.last_heal = Some(Instant::now());
        self.backoff = (self.backoff * 2).min(MAX_BACKOFF);

        let mut s = rng.seed_bytes();
        s[0] ^= (self.heal_count as u8).wrapping_mul(0x9e);
        s[1] ^= ((self.heal_count >> 8) & 0xff) as u8;
        s[2] ^= ((self.heal_count >> 16) & 0xff) as u8;
        rng.reseed(s);
    }

    /// Notify the controller that traffic succeeded (reset backoff).
    pub fn record_success(&mut self) {
        self.backoff = MIN_BACKOFF;
    }

    /// Current backoff duration.
    pub fn backoff(&self) -> Duration {
        self.backoff
    }
}

impl Default for SelfHealing {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::client::policy::NetworkEnvironment;

    #[test]
    fn should_heal_false_when_no_transports() {
        let mgr = TransportManager::new();
        let sh = SelfHealing::new();
        // No transports registered = no blocked transports = no heal.
        assert!(!sh.should_heal(&mgr));
    }

    #[test]
    fn heal_reseeds_and_prefers_healthy() {
        let mut rng = SeedRng::new([1u8; 32]);
        let before = rng.next_u64();
        let policy = TransportPolicy::new(NetworkEnvironment::Wifi);
        let mgr = TransportManager::new();
        let mut sh = SelfHealing::new();

        let order = sh.heal(&mut rng, &policy, &mgr);
        assert_eq!(sh.heal_count, 1);
        assert_ne!(rng.next_u64(), before);
        // No transports registered, so empty result.
        assert!(order.is_empty());
    }

    #[test]
    fn backoff_doubles_after_heal() {
        let mut rng = SeedRng::new([2u8; 32]);
        let policy = TransportPolicy::new(NetworkEnvironment::Unknown);
        let mgr = TransportManager::new();
        let mut sh = SelfHealing::new();

        assert_eq!(sh.backoff(), MIN_BACKOFF);
        sh.heal(&mut rng, &policy, &mgr);
        assert_eq!(sh.backoff(), MIN_BACKOFF * 2);
        sh.heal(&mut rng, &policy, &mgr);
        assert_eq!(sh.backoff(), MIN_BACKOFF * 4);
    }

    #[test]
    fn success_resets_backoff() {
        let mut rng = SeedRng::new([3u8; 32]);
        let policy = TransportPolicy::new(NetworkEnvironment::Unknown);
        let mgr = TransportManager::new();
        let mut sh = SelfHealing::new();

        sh.heal(&mut rng, &policy, &mgr);
        sh.heal(&mut rng, &policy, &mgr);
        assert!(sh.backoff() > MIN_BACKOFF);

        sh.record_success();
        assert_eq!(sh.backoff(), MIN_BACKOFF);
    }
}