use std::fmt;
use std::num::NonZeroU64;
use std::rc::Rc;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use crate::constants;
use crate::core::Timestamp;
use crate::error::ConfigError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TimingProfile {
passive_keepalive: Duration,
dead_timeout: Duration,
}
impl TimingProfile {
pub const V1: Self = Self {
passive_keepalive: constants::KEEPALIVE_TIMEOUT,
dead_timeout: constants::DEAD_TIMEOUT,
};
pub fn try_new(
passive_keepalive: Duration,
dead_timeout: Duration,
) -> Result<Self, TimingProfileError> {
if passive_keepalive < constants::PERSISTENT_KEEPALIVE_MIN {
return Err(TimingProfileError::KeepaliveTooShort);
}
let minimum_dead = passive_keepalive
.checked_mul(2)
.and_then(|twice| twice.checked_add(constants::K_INITIAL_RTT))
.and_then(|with_rtt| with_rtt.checked_add(constants::SHELL_LATENESS_BOUND))
.and_then(|with_first_lateness| {
with_first_lateness.checked_add(constants::SHELL_LATENESS_BOUND)
})
.ok_or(TimingProfileError::ArithmeticOverflow)?;
if dead_timeout <= minimum_dead {
return Err(TimingProfileError::DeadTimeoutTooShort);
}
if Instant::now().checked_add(dead_timeout).is_none() {
return Err(TimingProfileError::ArithmeticOverflow);
}
Ok(Self {
passive_keepalive,
dead_timeout,
})
}
pub const fn passive_keepalive(&self) -> Duration {
self.passive_keepalive
}
pub const fn dead_timeout(&self) -> Duration {
self.dead_timeout
}
}
impl Default for TimingProfile {
fn default() -> Self {
Self::V1
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum TimingProfileError {
#[error("the passive-keepalive interval is below the 1 s floor")]
KeepaliveTooShort,
#[error(
"the dead timeout is too short for two keepalives, initial RTT, and two shell-lateness allowances"
)]
DeadTimeoutTooShort,
#[error("the timing profile overflows duration or monotonic-instant arithmetic")]
ArithmeticOverflow,
}
pub trait WallClock {
fn now(&self) -> Timestamp;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SystemClock;
impl WallClock for SystemClock {
fn now(&self) -> Timestamp {
match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(d) => Timestamp::new(d.as_secs(), d.subsec_nanos()),
Err(_) => Timestamp::new(0, 0),
}
}
}
#[derive(Clone)]
pub struct Config {
intro_queue_cap: usize,
intro_max_per_source: usize,
epoch_size: NonZeroU64,
stream_window: u64,
connection_window: u64,
timing_profile: TimingProfile,
clock: Rc<dyn WallClock>,
}
impl fmt::Debug for Config {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Config")
.field("intro_queue_cap", &self.intro_queue_cap)
.field("intro_max_per_source", &self.intro_max_per_source)
.field("epoch_size", &self.epoch_size)
.field("stream_window", &self.stream_window)
.field("connection_window", &self.connection_window)
.field("timing_profile", &self.timing_profile)
.finish_non_exhaustive()
}
}
impl Default for Config {
fn default() -> Self {
Self {
intro_queue_cap: constants::INTRO_QUEUE_CAP,
intro_max_per_source: constants::INTRO_MAX_PER_SOURCE,
epoch_size: Config::DEFAULT_EPOCH_SIZE,
stream_window: Config::DEFAULT_STREAM_WINDOW,
connection_window: Config::DEFAULT_CONNECTION_WINDOW,
timing_profile: TimingProfile::default(),
clock: Rc::new(SystemClock),
}
}
}
impl Config {
pub const DEFAULT_EPOCH_SIZE: NonZeroU64 = match NonZeroU64::new(constants::REKEY_EPOCH_MSGS) {
Some(n) => n,
None => panic!("REKEY_EPOCH_MSGS is nonzero"),
};
pub const DEFAULT_STREAM_WINDOW: u64 = constants::INITIAL_MAX_STREAM_DATA;
pub const DEFAULT_CONNECTION_WINDOW: u64 = constants::INITIAL_MAX_DATA;
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_intro_queue_cap(mut self, cap: usize) -> Self {
self.intro_queue_cap = cap;
self
}
#[must_use]
pub fn with_intro_max_per_source(mut self, cap: usize) -> Self {
self.intro_max_per_source = cap;
self
}
#[must_use]
pub fn with_epoch_size(mut self, epoch_size: NonZeroU64) -> Self {
self.epoch_size = epoch_size;
self
}
pub fn with_flow_windows(mut self, stream: u64, connection: u64) -> Result<Self, ConfigError> {
if stream < Self::DEFAULT_STREAM_WINDOW || connection < Self::DEFAULT_CONNECTION_WINDOW {
return Err(ConfigError::WindowTooSmall);
}
if stream > crate::varint::VarInt::MAX_VALUE
|| connection > crate::varint::VarInt::MAX_VALUE
{
return Err(ConfigError::WindowTooLarge);
}
if stream > connection {
return Err(ConfigError::StreamWindowAboveConnection);
}
self.stream_window = stream;
self.connection_window = connection;
Ok(self)
}
#[must_use]
pub fn with_timing_profile(mut self, timing_profile: TimingProfile) -> Self {
self.timing_profile = timing_profile;
self
}
#[must_use]
pub fn with_clock(mut self, clock: Rc<dyn WallClock>) -> Self {
self.clock = clock;
self
}
pub fn intro_queue_cap(&self) -> usize {
self.intro_queue_cap
}
pub fn intro_max_per_source(&self) -> usize {
self.intro_max_per_source
}
pub fn epoch_size(&self) -> NonZeroU64 {
self.epoch_size
}
pub fn stream_window(&self) -> u64 {
self.stream_window
}
pub fn connection_window(&self) -> u64 {
self.connection_window
}
pub fn timing_profile(&self) -> TimingProfile {
self.timing_profile
}
pub fn clock(&self) -> &dyn WallClock {
&*self.clock
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_default_epoch_size_is_rekey_epoch_msgs() {
assert_eq!(
Config::new().epoch_size().get(),
constants::REKEY_EPOCH_MSGS
);
assert_eq!(
Config::DEFAULT_EPOCH_SIZE.get(),
constants::REKEY_EPOCH_MSGS
);
}
#[test]
fn the_epoch_size_override_takes_effect() {
let config = Config::new().with_epoch_size(NonZeroU64::new(8).unwrap());
assert_eq!(config.epoch_size().get(), 8);
}
#[test]
fn the_default_windows_are_the_ratified_constants() {
let config = Config::new();
assert_eq!(config.stream_window(), constants::INITIAL_MAX_STREAM_DATA);
assert_eq!(config.connection_window(), constants::INITIAL_MAX_DATA);
assert_eq!(
Config::DEFAULT_STREAM_WINDOW,
constants::INITIAL_MAX_STREAM_DATA
);
assert_eq!(
Config::DEFAULT_CONNECTION_WINDOW,
constants::INITIAL_MAX_DATA
);
}
#[test]
fn the_window_override_takes_effect() {
let config = Config::new()
.with_flow_windows(1 << 20, 1 << 23)
.expect("a raise");
assert_eq!(config.stream_window(), 1 << 20);
assert_eq!(config.connection_window(), 1 << 23);
}
#[test]
fn a_window_below_its_ratified_default_is_refused() {
assert_eq!(
Config::new()
.with_flow_windows(
constants::INITIAL_MAX_STREAM_DATA - 1,
constants::INITIAL_MAX_DATA,
)
.unwrap_err(),
ConfigError::WindowTooSmall,
);
assert_eq!(
Config::new()
.with_flow_windows(
constants::INITIAL_MAX_STREAM_DATA,
constants::INITIAL_MAX_DATA - 1,
)
.unwrap_err(),
ConfigError::WindowTooSmall,
);
let at_the_defaults = Config::new()
.with_flow_windows(
constants::INITIAL_MAX_STREAM_DATA,
constants::INITIAL_MAX_DATA,
)
.expect("the defaults restated are a legal no-op raise");
assert_eq!(
at_the_defaults.stream_window(),
constants::INITIAL_MAX_STREAM_DATA
);
}
#[test]
fn a_window_past_the_varint_bound_is_refused() {
let max = crate::varint::VarInt::MAX_VALUE;
assert_eq!(
Config::new()
.with_flow_windows(max, max)
.unwrap()
.stream_window(),
max,
"the largest encodable absolute offset is admissible",
);
assert_eq!(
Config::new()
.with_flow_windows(max + 1, max + 1)
.unwrap_err(),
ConfigError::WindowTooLarge,
);
assert_eq!(
Config::new().with_flow_windows(max, max + 1).unwrap_err(),
ConfigError::WindowTooLarge,
"the connection window is checked on its own, not only via the pair",
);
}
#[test]
fn the_stream_window_may_not_exceed_the_connection_window() {
assert_eq!(
Config::new()
.with_flow_windows(1 << 23, 1 << 20)
.unwrap_err(),
ConfigError::StreamWindowAboveConnection,
);
}
}