use std::{
collections::hash_map::RandomState,
fmt::Display,
io::Cursor,
net::{AddrParseError, IpAddr},
sync::Arc,
time::{Duration, Instant},
};
use serde::{Deserialize, Deserializer, de};
use crate::{
KeySet, NoCipher, NtpClock, NtpPacket, NtpTimestamp, NtpVersion, PacketParsingError,
SystemSnapshot, ipfilter::IpFilter,
};
pub enum ServerAction<'a> {
Ignore,
Respond { message: &'a [u8] },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ServerReason {
RateLimit,
ParseError,
InvalidCrypto,
InternalError,
Policy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ServerResponse {
NTSNak,
Deny,
Ignore,
ProvideTime,
}
pub trait ServerStatHandler {
fn register(&mut self, version: u8, nts: bool, reason: ServerReason, response: ServerResponse);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum FilterAction {
Ignore,
Deny,
}
impl From<FilterAction> for ServerResponse {
fn from(value: FilterAction) -> Self {
match value {
FilterAction::Ignore => ServerResponse::Ignore,
FilterAction::Deny => ServerResponse::Deny,
}
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Deserialize)]
pub struct FilterList {
pub filter: Vec<IpSubnet>,
pub action: FilterAction,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ServerConfig {
pub denylist: FilterList,
pub allowlist: FilterList,
pub rate_limiting_cache_size: usize,
pub rate_limiting_cutoff: Duration,
pub require_nts: Option<FilterAction>,
pub accepted_versions: Vec<NtpVersion>,
}
pub struct Server<C> {
config: ServerConfig,
clock: C,
denyfilter: IpFilter,
allowfilter: IpFilter,
client_cache: TimestampedCache<IpAddr>,
system: SystemSnapshot,
keyset: Arc<KeySet>,
}
fn fallback_message_version(message: &[u8]) -> u8 {
message.first().map_or(0, |v| (v & 0b0011_1000) >> 3)
}
impl<C> Server<C> {
pub fn new(
config: ServerConfig,
clock: C,
system: SystemSnapshot,
keyset: Arc<KeySet>,
) -> Self {
let denyfilter = IpFilter::new(&config.denylist.filter);
let allowfilter = IpFilter::new(&config.allowlist.filter);
let client_cache = TimestampedCache::new(config.rate_limiting_cache_size);
Self {
config,
clock,
denyfilter,
allowfilter,
client_cache,
system,
keyset,
}
}
pub fn update_config(&mut self, config: ServerConfig) {
if self.config.denylist.filter != config.denylist.filter {
self.denyfilter = IpFilter::new(&config.denylist.filter);
}
if self.config.allowlist.filter != config.allowlist.filter {
self.allowfilter = IpFilter::new(&config.allowlist.filter);
}
if self.config.rate_limiting_cache_size != config.rate_limiting_cache_size {
self.client_cache = TimestampedCache::new(config.rate_limiting_cache_size);
}
self.config = config;
}
pub fn update_system(&mut self, system: SystemSnapshot) {
self.system = system;
}
pub fn update_keyset(&mut self, keyset: Arc<KeySet>) {
self.keyset = keyset;
}
fn intended_action(&mut self, client_ip: IpAddr) -> (ServerResponse, ServerReason) {
if self.denyfilter.is_in(&client_ip) {
(self.config.denylist.action.into(), ServerReason::Policy)
} else if !self.allowfilter.is_in(&client_ip) {
(self.config.allowlist.action.into(), ServerReason::Policy)
} else if !self.client_cache.is_allowed(
client_ip,
Instant::now(),
self.config.rate_limiting_cutoff,
) {
(ServerResponse::Ignore, ServerReason::RateLimit)
} else {
(ServerResponse::ProvideTime, ServerReason::Policy)
}
}
}
impl<C: NtpClock> Server<C> {
#[expect(clippy::too_many_lines)]
pub fn handle<'a>(
&mut self,
client_ip: IpAddr,
recv_timestamp: NtpTimestamp,
message: &[u8],
buffer: &'a mut [u8],
stats_handler: &mut impl ServerStatHandler,
) -> ServerAction<'a> {
let (mut action, mut reason) = self.intended_action(client_ip);
if action == ServerResponse::Ignore {
stats_handler.register(fallback_message_version(message), false, reason, action);
return ServerAction::Ignore;
}
let (packet, cookie) = match NtpPacket::deserialize(message, self.keyset.as_ref()) {
Ok((packet, cookie)) => match packet.mode() {
crate::NtpAssociationMode::Client => (packet, cookie),
_ => {
stats_handler.register(
fallback_message_version(message),
false,
ServerReason::ParseError,
ServerResponse::Ignore,
);
return ServerAction::Ignore;
}
},
Err(PacketParsingError::DecryptError(packet)) => {
if action != ServerResponse::Deny {
action = ServerResponse::NTSNak;
reason = ServerReason::InvalidCrypto;
}
(packet, None)
}
Err(_) => {
stats_handler.register(
fallback_message_version(message),
false,
ServerReason::ParseError,
ServerResponse::Ignore,
);
return ServerAction::Ignore;
}
};
let version = packet.version();
if !self.config.accepted_versions.contains(&version) {
stats_handler.register(
version.as_u8(),
false,
ServerReason::Policy,
ServerResponse::Ignore,
);
return ServerAction::Ignore;
}
let nts = cookie.is_some() || action == ServerResponse::NTSNak;
if let (false, Some(non_nts_action)) = (nts, self.config.require_nts) {
if non_nts_action == FilterAction::Ignore {
stats_handler.register(
version.into(),
nts,
ServerReason::Policy,
ServerResponse::Ignore,
);
return ServerAction::Ignore;
}
action = ServerResponse::Deny;
reason = ServerReason::Policy;
}
let mut cursor = Cursor::new(buffer);
let result = match action {
ServerResponse::NTSNak => {
NtpPacket::nts_nak_response(packet).serialize(&mut cursor, &NoCipher, None)
}
ServerResponse::Deny => {
if let Some(cookie) = cookie {
NtpPacket::nts_deny_response(packet).serialize(
&mut cursor,
cookie.s2c.as_ref(),
None,
)
} else {
NtpPacket::deny_response(packet).serialize(&mut cursor, &NoCipher, None)
}
}
ServerResponse::ProvideTime => {
if let Some(cookie) = cookie {
NtpPacket::nts_timestamp_response(
&self.system,
packet,
recv_timestamp,
&self.clock,
&cookie,
&self.keyset,
)
.serialize(
&mut cursor,
cookie.s2c.as_ref(),
Some(message.len()),
)
} else {
NtpPacket::timestamp_response(&self.system, packet, recv_timestamp, &self.clock)
.serialize(&mut cursor, &NoCipher, Some(message.len()))
}
}
ServerResponse::Ignore => unreachable!(),
};
match result {
Ok(_) => {
stats_handler.register(version.into(), nts, reason, action);
let length = cursor.position();
ServerAction::Respond {
message: &cursor.into_inner()[..length as _],
}
}
Err(e) => {
tracing::error!("Could not serialize response: {}", e);
stats_handler.register(
version.into(),
nts,
ServerReason::InternalError,
ServerResponse::Ignore,
);
ServerAction::Ignore
}
}
}
}
#[derive(Debug)]
struct TimestampedCache<T> {
randomstate: RandomState,
elements: Vec<Option<(T, Instant)>>,
}
impl<T: std::hash::Hash + Eq> TimestampedCache<T> {
fn new(length: usize) -> Self {
Self {
elements: std::iter::repeat_with(|| None).take(length).collect(),
randomstate: RandomState::new(),
}
}
fn index(&self, item: &T) -> usize {
use std::hash::BuildHasher;
self.randomstate.hash_one(item) as usize % self.elements.len()
}
fn is_allowed(&mut self, item: T, timestamp: Instant, cutoff: Duration) -> bool {
if self.elements.is_empty() {
return true;
}
let index = self.index(&item);
let timestamp_if_same = self.elements[index]
.as_ref()
.and_then(|(v, t)| (&item == v).then_some(t))
.copied();
self.elements[index] = Some((item, timestamp));
if let Some(old_timestamp) = timestamp_if_same {
timestamp.duration_since(old_timestamp) >= cutoff
} else {
true
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct IpSubnet {
pub addr: IpAddr,
pub mask: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SubnetParseError {
Subnet,
Ip(AddrParseError),
Mask,
}
impl std::error::Error for SubnetParseError {}
impl Display for SubnetParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Subnet => write!(f, "Invalid subnet syntax"),
Self::Ip(e) => write!(f, "{e} in subnet"),
Self::Mask => write!(f, "Invalid subnet mask"),
}
}
}
impl From<AddrParseError> for SubnetParseError {
fn from(value: AddrParseError) -> Self {
Self::Ip(value)
}
}
impl std::str::FromStr for IpSubnet {
type Err = SubnetParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (addr, mask) = s.split_once('/').ok_or(SubnetParseError::Subnet)?;
let addr: IpAddr = addr.parse()?;
let mask: u8 = mask.parse().map_err(|_| SubnetParseError::Mask)?;
let max_mask = match addr {
IpAddr::V4(_) => 32,
IpAddr::V6(_) => 128,
};
if mask > max_mask {
return Err(SubnetParseError::Mask);
}
Ok(IpSubnet { addr, mask })
}
}
impl<'de> Deserialize<'de> for IpSubnet {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
std::str::FromStr::from_str(&s).map_err(de::Error::custom)
}
}
#[cfg(test)]
#[expect(
clippy::too_many_lines,
reason = "Long tests are not really a big problem"
)]
mod tests {
use std::net::{Ipv4Addr, Ipv6Addr};
use crate::{
Cipher, DecodedServerCookie, KeySetProvider, NtpDuration, NtpLeapIndicator,
PollIntervalLimits, nts::AeadAlgorithm, packet::AesSivCmac256,
};
use super::*;
#[derive(Debug, Clone, Default)]
struct TestClock {
cur: NtpTimestamp,
}
impl NtpClock for TestClock {
type Error = std::time::SystemTimeError;
fn now(&self) -> std::result::Result<NtpTimestamp, Self::Error> {
Ok(self.cur)
}
fn set_frequency(&self, _freq: f64) -> Result<NtpTimestamp, Self::Error> {
panic!("Shouldn't be called by server");
}
fn get_frequency(&self) -> Result<f64, Self::Error> {
Ok(0.0)
}
fn step_clock(&self, _offset: NtpDuration) -> Result<NtpTimestamp, Self::Error> {
panic!("Shouldn't be called by server");
}
fn disable_ntp_algorithm(&self) -> Result<(), Self::Error> {
panic!("Shouldn't be called by server");
}
fn error_estimate_update(
&self,
_est_error: NtpDuration,
_max_error: NtpDuration,
) -> Result<(), Self::Error> {
panic!("Shouldn't be called by server");
}
fn status_update(&self, _leap_status: NtpLeapIndicator) -> Result<(), Self::Error> {
panic!("Shouldn't be called by source");
}
}
#[derive(Debug, Default)]
struct TestStatHandler {
last_register: Option<(u8, bool, ServerReason, ServerResponse)>,
}
impl ServerStatHandler for TestStatHandler {
fn register(
&mut self,
version: u8,
nts: bool,
reason: ServerReason,
response: ServerResponse,
) {
assert!(self.last_register.is_none());
self.last_register = Some((version, nts, reason, response));
}
}
fn serialize_packet_unencrypted(send_packet: &NtpPacket) -> Vec<u8> {
let mut buf = vec![0; 1024];
let mut cursor = Cursor::new(buf.as_mut_slice());
send_packet.serialize(&mut cursor, &NoCipher, None).unwrap();
let end = cursor.position() as usize;
buf.truncate(end);
buf
}
fn serialize_packet_encrypted(send_packet: &NtpPacket, key: &dyn Cipher) -> Vec<u8> {
let mut buf = vec![0; 1024];
let mut cursor = Cursor::new(buf.as_mut_slice());
send_packet.serialize(&mut cursor, key, None).unwrap();
let end = cursor.position() as usize;
buf.truncate(end);
buf
}
#[test]
fn test_server_allow_filter() {
let config = ServerConfig {
denylist: FilterList {
filter: vec![],
action: FilterAction::Deny,
},
allowlist: FilterList {
filter: vec!["127.0.0.0/24".parse().unwrap()],
action: FilterAction::Ignore,
},
rate_limiting_cutoff: Duration::from_secs(1),
rate_limiting_cache_size: 0,
require_nts: None,
accepted_versions: vec![NtpVersion::V4],
};
let clock = TestClock {
cur: NtpTimestamp::from_fixed_int(200),
};
let mut stats = TestStatHandler::default();
let mut server = Server::new(
config,
clock,
SystemSnapshot::default(),
KeySetProvider::new(1).get(),
);
let (packet, id) = NtpPacket::poll_message(PollIntervalLimits::default().min);
let serialized = serialize_packet_unencrypted(&packet);
let mut buf = [0; 48];
let response = server.handle(
"127.0.0.1".parse().unwrap(),
NtpTimestamp::from_fixed_int(100),
&serialized,
&mut buf,
&mut stats,
);
assert_eq!(
stats.last_register.take(),
Some((4, false, ServerReason::Policy, ServerResponse::ProvideTime))
);
let data = match response {
ServerAction::Ignore => panic!("Server ignored packet"),
ServerAction::Respond { message } => message,
};
let packet = NtpPacket::deserialize(data, &NoCipher).unwrap().0;
assert_ne!(packet.stratum(), 0);
assert!(packet.valid_server_response(id, false));
assert_eq!(
packet.receive_timestamp(),
NtpTimestamp::from_fixed_int(100)
);
assert_eq!(
packet.transmit_timestamp(),
NtpTimestamp::from_fixed_int(200)
);
let mut buf = [0; 48];
let response = server.handle(
"128.0.0.1".parse().unwrap(),
NtpTimestamp::from_fixed_int(100),
&serialized,
&mut buf,
&mut stats,
);
assert_eq!(
stats.last_register.take(),
Some((4, false, ServerReason::Policy, ServerResponse::Ignore))
);
assert!(matches!(response, ServerAction::Ignore));
let config = ServerConfig {
denylist: FilterList {
filter: vec![],
action: FilterAction::Deny,
},
allowlist: FilterList {
filter: vec!["127.0.0.0/24".parse().unwrap()],
action: FilterAction::Deny,
},
rate_limiting_cutoff: Duration::from_secs(1),
rate_limiting_cache_size: 0,
require_nts: None,
accepted_versions: vec![NtpVersion::V4],
};
server.update_config(config);
let mut buf = [0; 48];
let response = server.handle(
"128.0.0.1".parse().unwrap(),
NtpTimestamp::from_fixed_int(100),
&serialized,
&mut buf,
&mut stats,
);
assert_eq!(
stats.last_register.take(),
Some((4, false, ServerReason::Policy, ServerResponse::Deny))
);
let data = match response {
ServerAction::Ignore => panic!("Server ignored packet"),
ServerAction::Respond { message } => message,
};
let packet = NtpPacket::deserialize(data, &NoCipher).unwrap().0;
assert!(packet.valid_server_response(id, false));
assert!(packet.is_kiss_deny());
}
#[test]
fn test_server_deny_filter() {
let config = ServerConfig {
denylist: FilterList {
filter: vec!["128.0.0.0/24".parse().unwrap()],
action: FilterAction::Deny,
},
allowlist: FilterList {
filter: vec!["0.0.0.0/0".parse().unwrap()],
action: FilterAction::Ignore,
},
rate_limiting_cutoff: Duration::from_secs(1),
rate_limiting_cache_size: 0,
require_nts: None,
accepted_versions: vec![NtpVersion::V4],
};
let clock = TestClock {
cur: NtpTimestamp::from_fixed_int(200),
};
let mut stats = TestStatHandler::default();
let mut server = Server::new(
config,
clock,
SystemSnapshot::default(),
KeySetProvider::new(1).get(),
);
let (packet, id) = NtpPacket::poll_message(PollIntervalLimits::default().min);
let serialized = serialize_packet_unencrypted(&packet);
let mut buf = [0; 48];
let response = server.handle(
"127.0.0.1".parse().unwrap(),
NtpTimestamp::from_fixed_int(100),
&serialized,
&mut buf,
&mut stats,
);
assert_eq!(
stats.last_register.take(),
Some((4, false, ServerReason::Policy, ServerResponse::ProvideTime))
);
let data = match response {
ServerAction::Ignore => panic!("Server ignored packet"),
ServerAction::Respond { message } => message,
};
let packet = NtpPacket::deserialize(data, &NoCipher).unwrap().0;
assert_ne!(packet.stratum(), 0);
assert!(packet.valid_server_response(id, false));
assert_eq!(
packet.receive_timestamp(),
NtpTimestamp::from_fixed_int(100)
);
assert_eq!(
packet.transmit_timestamp(),
NtpTimestamp::from_fixed_int(200)
);
let mut buf = [0; 48];
let response = server.handle(
"128.0.0.1".parse().unwrap(),
NtpTimestamp::from_fixed_int(100),
&serialized,
&mut buf,
&mut stats,
);
assert_eq!(
stats.last_register.take(),
Some((4, false, ServerReason::Policy, ServerResponse::Deny))
);
let data = match response {
ServerAction::Ignore => panic!("Server ignored packet"),
ServerAction::Respond { message } => message,
};
let packet = NtpPacket::deserialize(data, &NoCipher).unwrap().0;
assert!(packet.valid_server_response(id, false));
assert!(packet.is_kiss_deny());
let config = ServerConfig {
denylist: FilterList {
filter: vec!["128.0.0.0/24".parse().unwrap()],
action: FilterAction::Ignore,
},
allowlist: FilterList {
filter: vec!["0.0.0.0/0".parse().unwrap()],
action: FilterAction::Ignore,
},
rate_limiting_cutoff: Duration::from_secs(1),
rate_limiting_cache_size: 0,
require_nts: None,
accepted_versions: vec![NtpVersion::V4],
};
server.update_config(config);
let mut buf = [0; 48];
let response = server.handle(
"128.0.0.1".parse().unwrap(),
NtpTimestamp::from_fixed_int(100),
&serialized,
&mut buf,
&mut stats,
);
assert_eq!(
stats.last_register.take(),
Some((4, false, ServerReason::Policy, ServerResponse::Ignore))
);
assert!(matches!(response, ServerAction::Ignore));
}
#[test]
fn test_server_rate_limit() {
let config = ServerConfig {
denylist: FilterList {
filter: vec![],
action: FilterAction::Deny,
},
allowlist: FilterList {
filter: vec!["0.0.0.0/0".parse().unwrap()],
action: FilterAction::Ignore,
},
rate_limiting_cutoff: Duration::from_millis(100),
rate_limiting_cache_size: 32,
require_nts: None,
accepted_versions: vec![NtpVersion::V4],
};
let clock = TestClock {
cur: NtpTimestamp::from_fixed_int(200),
};
let mut stats = TestStatHandler::default();
let mut server = Server::new(
config,
clock,
SystemSnapshot::default(),
KeySetProvider::new(1).get(),
);
let (packet, id) = NtpPacket::poll_message(PollIntervalLimits::default().min);
let serialized = serialize_packet_unencrypted(&packet);
let mut buf = [0; 48];
let response = server.handle(
"127.0.0.1".parse().unwrap(),
NtpTimestamp::from_fixed_int(100),
&serialized,
&mut buf,
&mut stats,
);
assert_eq!(
stats.last_register.take(),
Some((4, false, ServerReason::Policy, ServerResponse::ProvideTime))
);
let data = match response {
ServerAction::Ignore => panic!("Server ignored packet"),
ServerAction::Respond { message } => message,
};
let packet = NtpPacket::deserialize(data, &NoCipher).unwrap().0;
assert_ne!(packet.stratum(), 0);
assert!(packet.valid_server_response(id, false));
assert_eq!(
packet.receive_timestamp(),
NtpTimestamp::from_fixed_int(100)
);
assert_eq!(
packet.transmit_timestamp(),
NtpTimestamp::from_fixed_int(200)
);
let mut buf = [0; 48];
let response = server.handle(
"127.0.0.1".parse().unwrap(),
NtpTimestamp::from_fixed_int(100),
&serialized,
&mut buf,
&mut stats,
);
assert_eq!(
stats.last_register.take(),
Some((4, false, ServerReason::RateLimit, ServerResponse::Ignore))
);
assert!(matches!(response, ServerAction::Ignore));
std::thread::sleep(std::time::Duration::from_millis(120));
let mut buf = [0; 48];
let response = server.handle(
"127.0.0.1".parse().unwrap(),
NtpTimestamp::from_fixed_int(100),
&serialized,
&mut buf,
&mut stats,
);
assert_eq!(
stats.last_register.take(),
Some((4, false, ServerReason::Policy, ServerResponse::ProvideTime))
);
let data = match response {
ServerAction::Ignore => panic!("Server ignored packet"),
ServerAction::Respond { message } => message,
};
let packet = NtpPacket::deserialize(data, &NoCipher).unwrap().0;
assert_ne!(packet.stratum(), 0);
assert!(packet.valid_server_response(id, false));
assert_eq!(
packet.receive_timestamp(),
NtpTimestamp::from_fixed_int(100)
);
assert_eq!(
packet.transmit_timestamp(),
NtpTimestamp::from_fixed_int(200)
);
let config = ServerConfig {
denylist: FilterList {
filter: vec![],
action: FilterAction::Deny,
},
allowlist: FilterList {
filter: vec!["0.0.0.0/0".parse().unwrap()],
action: FilterAction::Ignore,
},
rate_limiting_cutoff: Duration::from_millis(100),
rate_limiting_cache_size: 0,
require_nts: None,
accepted_versions: vec![NtpVersion::V4],
};
server.update_config(config);
let mut buf = [0; 48];
let response = server.handle(
"127.0.0.1".parse().unwrap(),
NtpTimestamp::from_fixed_int(100),
&serialized,
&mut buf,
&mut stats,
);
assert_eq!(
stats.last_register.take(),
Some((4, false, ServerReason::Policy, ServerResponse::ProvideTime))
);
let data = match response {
ServerAction::Ignore => panic!("Server ignored packet"),
ServerAction::Respond { message } => message,
};
let packet = NtpPacket::deserialize(data, &NoCipher).unwrap().0;
assert_ne!(packet.stratum(), 0);
assert!(packet.valid_server_response(id, false));
assert_eq!(
packet.receive_timestamp(),
NtpTimestamp::from_fixed_int(100)
);
assert_eq!(
packet.transmit_timestamp(),
NtpTimestamp::from_fixed_int(200)
);
let mut buf = [0; 48];
let response = server.handle(
"127.0.0.1".parse().unwrap(),
NtpTimestamp::from_fixed_int(100),
&serialized,
&mut buf,
&mut stats,
);
assert_eq!(
stats.last_register.take(),
Some((4, false, ServerReason::Policy, ServerResponse::ProvideTime))
);
let data = match response {
ServerAction::Ignore => panic!("Server ignored packet"),
ServerAction::Respond { message } => message,
};
let packet = NtpPacket::deserialize(data, &NoCipher).unwrap().0;
assert_ne!(packet.stratum(), 0);
assert!(packet.valid_server_response(id, false));
assert_eq!(
packet.receive_timestamp(),
NtpTimestamp::from_fixed_int(100)
);
assert_eq!(
packet.transmit_timestamp(),
NtpTimestamp::from_fixed_int(200)
);
}
#[test]
fn test_server_ignores_non_request() {
let config = ServerConfig {
denylist: FilterList {
filter: vec![],
action: FilterAction::Deny,
},
allowlist: FilterList {
filter: vec!["0.0.0.0/0".parse().unwrap()],
action: FilterAction::Ignore,
},
rate_limiting_cutoff: Duration::from_millis(100),
rate_limiting_cache_size: 0,
require_nts: None,
accepted_versions: vec![NtpVersion::V4],
};
let clock = TestClock {
cur: NtpTimestamp::from_fixed_int(200),
};
let mut stats = TestStatHandler::default();
let mut server = Server::new(
config,
clock,
SystemSnapshot::default(),
KeySetProvider::new(1).get(),
);
let (packet, _) = NtpPacket::poll_message(PollIntervalLimits::default().min);
let mut serialized = serialize_packet_unencrypted(&packet);
for version in 0..8 {
for mode in 0..8 {
if mode == 3 {
continue;
}
serialized[0] = (serialized[0] & 0xC0) | (version << 3) | mode;
let mut buf = [0; 48];
let response = server.handle(
"127.0.0.1".parse().unwrap(),
NtpTimestamp::from_fixed_int(100),
&serialized,
&mut buf,
&mut stats,
);
stats.last_register.take();
assert!(matches!(response, ServerAction::Ignore));
}
}
}
#[test]
fn test_server_corrupted() {
let config = ServerConfig {
denylist: FilterList {
filter: vec![],
action: FilterAction::Deny,
},
allowlist: FilterList {
filter: vec!["0.0.0.0/0".parse().unwrap()],
action: FilterAction::Ignore,
},
rate_limiting_cutoff: Duration::from_millis(100),
rate_limiting_cache_size: 0,
require_nts: None,
accepted_versions: vec![NtpVersion::V4],
};
let clock = TestClock {
cur: NtpTimestamp::from_fixed_int(200),
};
let mut stats = TestStatHandler::default();
let mut server = Server::new(
config,
clock,
SystemSnapshot::default(),
KeySetProvider::new(1).get(),
);
let (packet, _) = NtpPacket::poll_message(PollIntervalLimits::default().min);
let mut serialized = serialize_packet_unencrypted(&packet);
let mut buf = [0; 1];
let response = server.handle(
"127.0.0.1".parse().unwrap(),
NtpTimestamp::from_fixed_int(100),
&serialized,
&mut buf,
&mut stats,
);
assert_eq!(
stats.last_register.take(),
Some((
4,
false,
ServerReason::InternalError,
ServerResponse::Ignore
))
);
assert!(matches!(response, ServerAction::Ignore));
serialized[0] = 42;
let mut buf = [0; 48];
let response = server.handle(
"127.0.0.1".parse().unwrap(),
NtpTimestamp::from_fixed_int(100),
&serialized,
&mut buf,
&mut stats,
);
assert_eq!(
stats.last_register.take(),
Some((5, false, ServerReason::ParseError, ServerResponse::Ignore))
);
assert!(matches!(response, ServerAction::Ignore));
let config = ServerConfig {
denylist: FilterList {
filter: vec![],
action: FilterAction::Deny,
},
allowlist: FilterList {
filter: vec!["128.0.0.0/24".parse().unwrap()],
action: FilterAction::Deny,
},
rate_limiting_cutoff: Duration::from_millis(100),
rate_limiting_cache_size: 0,
require_nts: None,
accepted_versions: vec![NtpVersion::V4],
};
server.update_config(config);
let mut buf = [0; 48];
let response = server.handle(
"127.0.0.1".parse().unwrap(),
NtpTimestamp::from_fixed_int(100),
&serialized,
&mut buf,
&mut stats,
);
assert_eq!(
stats.last_register.take(),
Some((5, false, ServerReason::ParseError, ServerResponse::Ignore))
);
assert!(matches!(response, ServerAction::Ignore));
let config = ServerConfig {
denylist: FilterList {
filter: vec![],
action: FilterAction::Deny,
},
allowlist: FilterList {
filter: vec!["128.0.0.0/24".parse().unwrap()],
action: FilterAction::Ignore,
},
rate_limiting_cutoff: Duration::from_millis(100),
rate_limiting_cache_size: 0,
require_nts: None,
accepted_versions: vec![NtpVersion::V4],
};
server.update_config(config);
let mut buf = [0; 48];
let response = server.handle(
"127.0.0.1".parse().unwrap(),
NtpTimestamp::from_fixed_int(100),
&serialized,
&mut buf,
&mut stats,
);
assert_eq!(
stats.last_register.take(),
Some((5, false, ServerReason::Policy, ServerResponse::Ignore))
);
assert!(matches!(response, ServerAction::Ignore));
let config = ServerConfig {
denylist: FilterList {
filter: vec!["127.0.0.0/24".parse().unwrap()],
action: FilterAction::Deny,
},
allowlist: FilterList {
filter: vec!["0.0.0.0/0".parse().unwrap()],
action: FilterAction::Ignore,
},
rate_limiting_cutoff: Duration::from_millis(100),
rate_limiting_cache_size: 0,
require_nts: None,
accepted_versions: vec![NtpVersion::V4],
};
server.update_config(config);
let mut buf = [0; 48];
let response = server.handle(
"127.0.0.1".parse().unwrap(),
NtpTimestamp::from_fixed_int(100),
&serialized,
&mut buf,
&mut stats,
);
assert_eq!(
stats.last_register.take(),
Some((5, false, ServerReason::ParseError, ServerResponse::Ignore))
);
assert!(matches!(response, ServerAction::Ignore));
let config = ServerConfig {
denylist: FilterList {
filter: vec!["127.0.0.0/24".parse().unwrap()],
action: FilterAction::Ignore,
},
allowlist: FilterList {
filter: vec!["0.0.0.0/0".parse().unwrap()],
action: FilterAction::Ignore,
},
rate_limiting_cutoff: Duration::from_millis(100),
rate_limiting_cache_size: 0,
require_nts: None,
accepted_versions: vec![NtpVersion::V4],
};
server.update_config(config);
let mut buf = [0; 48];
let response = server.handle(
"127.0.0.1".parse().unwrap(),
NtpTimestamp::from_fixed_int(100),
&serialized,
&mut buf,
&mut stats,
);
assert_eq!(
stats.last_register.take(),
Some((5, false, ServerReason::Policy, ServerResponse::Ignore))
);
assert!(matches!(response, ServerAction::Ignore));
}
#[test]
fn test_server_nts() {
let config = ServerConfig {
denylist: FilterList {
filter: vec![],
action: FilterAction::Deny,
},
allowlist: FilterList {
filter: vec!["0.0.0.0/0".parse().unwrap()],
action: FilterAction::Ignore,
},
rate_limiting_cutoff: Duration::from_millis(100),
rate_limiting_cache_size: 0,
require_nts: Some(FilterAction::Ignore),
accepted_versions: vec![NtpVersion::V4],
};
let clock = TestClock {
cur: NtpTimestamp::from_fixed_int(200),
};
let mut stats = TestStatHandler::default();
let keyset = KeySetProvider::new(1).get();
let mut server = Server::new(config, clock, SystemSnapshot::default(), keyset.clone());
let decodedcookie = DecodedServerCookie {
algorithm: AeadAlgorithm::AeadAesSivCmac256,
s2c: Box::new(AesSivCmac256::new([0; 32].into())),
c2s: Box::new(AesSivCmac256::new([0; 32].into())),
};
let cookie = keyset.encode_cookie(&decodedcookie);
let (packet, id) =
NtpPacket::nts_poll_message(&cookie, 0, PollIntervalLimits::default().min);
let serialized = serialize_packet_encrypted(&packet, decodedcookie.c2s.as_ref());
let mut buf = [0; 1024];
let response = server.handle(
"127.0.0.1".parse().unwrap(),
NtpTimestamp::from_fixed_int(100),
&serialized,
&mut buf,
&mut stats,
);
assert_eq!(
stats.last_register.take(),
Some((4, true, ServerReason::Policy, ServerResponse::ProvideTime))
);
let data = match response {
ServerAction::Ignore => panic!("Server ignored packet"),
ServerAction::Respond { message } => message,
};
let packet = NtpPacket::deserialize(data, decodedcookie.s2c.as_ref())
.unwrap()
.0;
assert_ne!(packet.stratum(), 0);
assert!(packet.valid_server_response(id, true));
assert_eq!(
packet.receive_timestamp(),
NtpTimestamp::from_fixed_int(100)
);
assert_eq!(
packet.transmit_timestamp(),
NtpTimestamp::from_fixed_int(200)
);
let cookie_invalid = KeySetProvider::new(1).get().encode_cookie(&decodedcookie);
let (packet_invalid, _) =
NtpPacket::nts_poll_message(&cookie_invalid, 0, PollIntervalLimits::default().min);
let serialized = serialize_packet_encrypted(&packet_invalid, decodedcookie.c2s.as_ref());
let mut buf = [0; 1024];
let response = server.handle(
"127.0.0.1".parse().unwrap(),
NtpTimestamp::from_fixed_int(100),
&serialized,
&mut buf,
&mut stats,
);
assert_eq!(
stats.last_register.take(),
Some((4, true, ServerReason::InvalidCrypto, ServerResponse::NTSNak))
);
let data = match response {
ServerAction::Ignore => panic!("Server ignored packet"),
ServerAction::Respond { message } => message,
};
let packet = NtpPacket::deserialize(data, decodedcookie.s2c.as_ref())
.unwrap()
.0;
assert!(packet.is_kiss_ntsn());
}
#[test]
fn test_server_require_nts() {
let mut config = ServerConfig {
denylist: FilterList {
filter: vec![],
action: FilterAction::Deny,
},
allowlist: FilterList {
filter: vec!["0.0.0.0/0".parse().unwrap()],
action: FilterAction::Ignore,
},
rate_limiting_cutoff: Duration::from_secs(1),
rate_limiting_cache_size: 0,
require_nts: Some(FilterAction::Ignore),
accepted_versions: vec![NtpVersion::V4],
};
let clock = TestClock {
cur: NtpTimestamp::from_fixed_int(200),
};
let mut stats = TestStatHandler::default();
let mut server = Server::new(
config.clone(),
clock,
SystemSnapshot::default(),
KeySetProvider::new(1).get(),
);
let (packet, _) = NtpPacket::poll_message(PollIntervalLimits::default().min);
let serialized = serialize_packet_unencrypted(&packet);
let mut buf = [0; 1024];
let response = server.handle(
"127.0.0.1".parse().unwrap(),
NtpTimestamp::from_fixed_int(100),
&serialized,
&mut buf,
&mut stats,
);
assert_eq!(
stats.last_register.take(),
Some((4, false, ServerReason::Policy, ServerResponse::Ignore))
);
assert!(matches!(response, ServerAction::Ignore));
let decodedcookie = DecodedServerCookie {
algorithm: AeadAlgorithm::AeadAesSivCmac256,
s2c: Box::new(AesSivCmac256::new([0; 32].into())),
c2s: Box::new(AesSivCmac256::new([0; 32].into())),
};
let cookie_invalid = KeySetProvider::new(1).get().encode_cookie(&decodedcookie);
let (packet_invalid, _) =
NtpPacket::nts_poll_message(&cookie_invalid, 0, PollIntervalLimits::default().min);
let serialized = serialize_packet_encrypted(&packet_invalid, decodedcookie.c2s.as_ref());
let response = server.handle(
"127.0.0.1".parse().unwrap(),
NtpTimestamp::from_fixed_int(100),
&serialized,
&mut buf,
&mut stats,
);
assert_eq!(
stats.last_register.take(),
Some((4, true, ServerReason::InvalidCrypto, ServerResponse::NTSNak))
);
let data = match response {
ServerAction::Ignore => panic!("Server ignored packet"),
ServerAction::Respond { message } => message,
};
let packet = NtpPacket::deserialize(data, decodedcookie.s2c.as_ref())
.unwrap()
.0;
assert!(packet.is_kiss_ntsn());
config.require_nts = Some(FilterAction::Deny);
server.update_config(config.clone());
let (packet, id) = NtpPacket::poll_message(PollIntervalLimits::default().min);
let serialized = serialize_packet_unencrypted(&packet);
let response = server.handle(
"127.0.0.1".parse().unwrap(),
NtpTimestamp::from_fixed_int(100),
&serialized,
&mut buf,
&mut stats,
);
assert_eq!(
stats.last_register.take(),
Some((4, false, ServerReason::Policy, ServerResponse::Deny))
);
let ServerAction::Respond { message } = response else {
panic!("Server ignored packet")
};
let packet = NtpPacket::deserialize(message, &NoCipher).unwrap().0;
assert!(packet.valid_server_response(id, false));
assert!(packet.is_kiss_deny());
}
#[test]
fn test_server_v5() {
let config = ServerConfig {
denylist: FilterList {
filter: vec![],
action: FilterAction::Deny,
},
allowlist: FilterList {
filter: vec!["127.0.0.0/24".parse().unwrap()],
action: FilterAction::Deny,
},
rate_limiting_cutoff: Duration::from_millis(100),
rate_limiting_cache_size: 0,
require_nts: None,
accepted_versions: vec![NtpVersion::V5],
};
let clock = TestClock {
cur: NtpTimestamp::from_fixed_int(200),
};
let mut stats = TestStatHandler::default();
let mut server = Server::new(
config,
clock,
SystemSnapshot::default(),
KeySetProvider::new(1).get(),
);
let (packet, id) = NtpPacket::poll_message_v5(PollIntervalLimits::default().min);
let serialized = serialize_packet_unencrypted(&packet);
let mut buf = [0; 1024];
let response = server.handle(
"127.0.0.1".parse().unwrap(),
NtpTimestamp::from_fixed_int(100),
&serialized,
&mut buf,
&mut stats,
);
assert_eq!(
stats.last_register.take(),
Some((5, false, ServerReason::Policy, ServerResponse::ProvideTime))
);
let data = match response {
ServerAction::Ignore => panic!("Server ignored packet"),
ServerAction::Respond { message } => message,
};
let packet = NtpPacket::deserialize(data, &NoCipher).unwrap().0;
assert_ne!(packet.stratum(), 0);
assert!(packet.valid_server_response(id, false));
assert_eq!(
packet.receive_timestamp(),
NtpTimestamp::from_fixed_int(100)
);
assert_eq!(
packet.transmit_timestamp(),
NtpTimestamp::from_fixed_int(200)
);
let mut buf = [0; 1024];
let response = server.handle(
"128.0.0.1".parse().unwrap(),
NtpTimestamp::from_fixed_int(100),
&serialized,
&mut buf,
&mut stats,
);
assert_eq!(
stats.last_register.take(),
Some((5, false, ServerReason::Policy, ServerResponse::Deny))
);
let data = match response {
ServerAction::Ignore => panic!("Server ignored packet"),
ServerAction::Respond { message } => message,
};
let packet = NtpPacket::deserialize(data, &NoCipher).unwrap().0;
assert!(packet.valid_server_response(id, false));
assert!(packet.is_kiss_deny());
}
#[test]
fn test_server_ignore_version() {
let config = ServerConfig {
denylist: FilterList {
filter: vec![],
action: FilterAction::Deny,
},
allowlist: FilterList {
filter: vec!["0.0.0.0/0".parse().unwrap()],
action: FilterAction::Ignore,
},
rate_limiting_cutoff: Duration::from_millis(1000),
rate_limiting_cache_size: 0,
require_nts: None,
accepted_versions: vec![NtpVersion::V3, NtpVersion::V4],
};
let clock = TestClock {
cur: NtpTimestamp::from_fixed_int(200),
};
let mut stats = TestStatHandler::default();
let mut server = Server::new(
config,
clock,
SystemSnapshot::default(),
KeySetProvider::new(1).get(),
);
let (packet, _) = NtpPacket::poll_message_v5(PollIntervalLimits::default().min);
let serialized = serialize_packet_unencrypted(&packet);
let mut buf = [0; 1024];
let response = server.handle(
"128.0.0.1".parse().unwrap(),
NtpTimestamp::from_fixed_int(100),
&serialized,
&mut buf,
&mut stats,
);
assert_eq!(
stats.last_register.take(),
Some((5, false, ServerReason::Policy, ServerResponse::Ignore))
);
assert!(matches!(response, ServerAction::Ignore));
server.update_config(ServerConfig {
denylist: FilterList {
filter: vec![],
action: FilterAction::Deny,
},
allowlist: FilterList {
filter: vec!["0.0.0.0/0".parse().unwrap()],
action: FilterAction::Ignore,
},
rate_limiting_cutoff: Duration::from_millis(100),
rate_limiting_cache_size: 0,
require_nts: None,
accepted_versions: vec![NtpVersion::V5],
});
let (packet, _) = NtpPacket::poll_message(PollIntervalLimits::default().min);
let serialized = serialize_packet_unencrypted(&packet);
let mut buf = [0; 1024];
let response = server.handle(
"128.0.0.1".parse().unwrap(),
NtpTimestamp::from_fixed_int(100),
&serialized,
&mut buf,
&mut stats,
);
assert_eq!(
stats.last_register.take(),
Some((4, false, ServerReason::Policy, ServerResponse::Ignore))
);
assert!(matches!(response, ServerAction::Ignore));
}
#[test]
fn timestamped_cache() {
let length = 8u8;
let mut cache: TimestampedCache<u8> = TimestampedCache::new(length as usize);
let second = Duration::from_secs(1);
let instant = Instant::now();
assert!(cache.is_allowed(0, instant, second));
assert!(!cache.is_allowed(0, instant, second));
let later = instant + 2 * second;
assert!(cache.is_allowed(0, later, second));
let even_later = later + 2 * second;
assert!(cache.is_allowed(length, even_later, second));
}
#[test]
fn timestamped_cache_size_0() {
let mut cache = TimestampedCache::new(0);
let second = Duration::from_secs(1);
let instant = Instant::now();
assert!(cache.is_allowed(0, instant, second));
}
#[test]
fn test_ipv4_subnet_parse() {
use std::str::FromStr;
assert!(matches!(
IpSubnet::from_str("bla/5"),
Err(SubnetParseError::Ip(_))
));
assert_eq!(IpSubnet::from_str("0.0.0.0"), Err(SubnetParseError::Subnet));
assert_eq!(
IpSubnet::from_str("0.0.0.0/33"),
Err(SubnetParseError::Mask)
);
assert_eq!(
IpSubnet::from_str("0.0.0.0/0"),
Ok(IpSubnet {
addr: IpAddr::V4(Ipv4Addr::UNSPECIFIED),
mask: 0
})
);
assert_eq!(
IpSubnet::from_str("127.0.0.1/32"),
Ok(IpSubnet {
addr: IpAddr::V4(Ipv4Addr::LOCALHOST),
mask: 32
})
);
assert_eq!(
serde_json::from_str::<IpSubnet>(r#""0.0.0.0/0""#).unwrap(),
IpSubnet {
addr: IpAddr::V4(Ipv4Addr::UNSPECIFIED),
mask: 0,
}
);
assert_eq!(
serde_json::from_str::<IpSubnet>(r#""127.0.0.1/32""#).unwrap(),
IpSubnet {
addr: IpAddr::V4(Ipv4Addr::LOCALHOST),
mask: 32,
}
);
}
#[test]
fn test_ipv6_subnet_parse() {
use std::str::FromStr;
assert!(matches!(
IpSubnet::from_str("bla/5"),
Err(SubnetParseError::Ip(_))
));
assert_eq!(IpSubnet::from_str("::"), Err(SubnetParseError::Subnet));
assert_eq!(IpSubnet::from_str("::/129"), Err(SubnetParseError::Mask));
assert_eq!(
IpSubnet::from_str("::/0"),
Ok(IpSubnet {
addr: IpAddr::V6(Ipv6Addr::UNSPECIFIED),
mask: 0
})
);
assert_eq!(
IpSubnet::from_str("::1/128"),
Ok(IpSubnet {
addr: IpAddr::V6(Ipv6Addr::LOCALHOST),
mask: 128
})
);
assert_eq!(
serde_json::from_str::<IpSubnet>(r#""::/0""#).unwrap(),
IpSubnet {
addr: IpAddr::V6(Ipv6Addr::UNSPECIFIED),
mask: 0,
}
);
assert_eq!(
serde_json::from_str::<IpSubnet>(r#""::1/128""#).unwrap(),
IpSubnet {
addr: IpAddr::V6(Ipv6Addr::LOCALHOST),
mask: 128,
}
);
}
}