use std::time::{Duration, Instant};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Protection {
#[default]
Unknown,
Open,
Blocked,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Change {
None,
Blocked,
Opened,
}
#[derive(Debug, Default)]
pub struct Guard {
state: Protection,
good: u32,
good_since: Option<Instant>,
hold_until: Option<Instant>,
}
impl Guard {
pub fn state(&self) -> Protection {
self.state
}
pub fn wake(&mut self, now: Instant, hold: Duration) -> Change {
self.good = 0;
self.good_since = None;
self.hold_until = Some(now + hold);
self.set(Protection::Blocked)
}
pub fn observe(&mut self, vpn_up: bool, now: Instant, resume_checks: u32, stable_for: Duration) -> Change {
if !vpn_up {
self.good = 0;
self.good_since = None;
return self.set(Protection::Blocked);
}
self.good = self.good.saturating_add(1);
let since = *self.good_since.get_or_insert(now);
if self.hold_until.is_some_and(|until| now < until) {
return self.set(Protection::Blocked);
}
self.hold_until = None;
match self.state {
Protection::Unknown => self.set(Protection::Open),
Protection::Open => Change::None,
Protection::Blocked if self.good >= resume_checks && now.duration_since(since) >= stable_for => {
self.set(Protection::Open)
}
Protection::Blocked => Change::None,
}
}
fn set(&mut self, new: Protection) -> Change {
if self.state == new {
return Change::None;
}
self.state = new;
match new {
Protection::Blocked => Change::Blocked,
_ => Change::Opened,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const HOLD: Duration = Duration::from_secs(5);
const NO_WAIT: Duration = Duration::ZERO;
#[test]
fn opens_on_first_good_check_and_blocks_immediately() {
let t = Instant::now();
let mut g = Guard::default();
assert_eq!(g.observe(true, t, 4, NO_WAIT), Change::Opened);
assert_eq!(g.observe(true, t, 4, NO_WAIT), Change::None);
assert_eq!(g.observe(false, t, 4, NO_WAIT), Change::Blocked);
assert_eq!(g.observe(false, t, 4, NO_WAIT), Change::None);
assert_eq!(g.state(), Protection::Blocked);
}
#[test]
fn needs_consecutive_good_checks_to_resume() {
let t = Instant::now();
let mut g = Guard::default();
g.observe(false, t, 4, NO_WAIT);
for _ in 0..3 {
assert_eq!(g.observe(true, t, 4, NO_WAIT), Change::None);
}
assert_eq!(g.observe(false, t, 4, NO_WAIT), Change::None, "a bad check resets the count");
for _ in 0..3 {
assert_eq!(g.observe(true, t, 4, NO_WAIT), Change::None);
}
assert_eq!(g.observe(true, t, 4, NO_WAIT), Change::Opened);
}
#[test]
fn a_burst_of_checks_does_not_resume_before_the_vpn_was_stable() {
let t = Instant::now();
let stable = Duration::from_millis(750);
let mut g = Guard::default();
g.observe(false, t, 4, stable);
for _ in 0..10 {
assert_eq!(g.observe(true, t, 4, stable), Change::None, "ten checks in one instant");
}
assert_eq!(g.observe(true, t + Duration::from_millis(749), 4, stable), Change::None);
assert_eq!(g.observe(true, t + stable, 4, stable), Change::Opened);
}
#[test]
fn wake_blocks_even_when_the_vpn_looks_fine() {
let t = Instant::now();
let mut g = Guard::default();
g.observe(true, t, 4, NO_WAIT);
assert_eq!(g.wake(t, HOLD), Change::Blocked);
for _ in 0..10 {
assert_eq!(g.observe(true, t + Duration::from_secs(2), 4, NO_WAIT), Change::None);
}
assert_eq!(g.state(), Protection::Blocked);
assert_eq!(g.observe(true, t + HOLD, 4, NO_WAIT), Change::Opened);
}
#[test]
fn wake_while_already_blocked_is_not_a_new_change() {
let t = Instant::now();
let mut g = Guard::default();
g.observe(false, t, 4, NO_WAIT);
assert_eq!(g.wake(t, HOLD), Change::None);
}
#[test]
fn down_during_hold_keeps_blocking() {
let t = Instant::now();
let mut g = Guard::default();
g.observe(true, t, 1, NO_WAIT);
g.wake(t, HOLD);
g.observe(false, t, 1, NO_WAIT);
assert_eq!(g.observe(true, t + HOLD, 1, NO_WAIT), Change::Opened, "good=1 after the hold is enough for resume_checks=1");
}
}