use std::io::{self, Result};
use std::time::Duration;
use async_trait::async_trait;
use tokio::net::UdpSocket;
use crate::pinger::Pinger;
use crate::uri::get_uri;
use crate::util::with_timeout;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
const DEFAULT_PORT: u16 = 123;
const PACKET_LEN: usize = 48;
const BUF_SIZE: usize = 0xFF;
const NTP_VERSION: u8 = 4;
const MODE_CLIENT: u8 = 3;
const MODE_SERVER: u8 = 4;
const MODE_BROADCAST: u8 = 5;
pub struct NtpPinger {
pub server: String,
pub timeout: Duration,
}
impl NtpPinger {
pub fn new(server: impl Into<String>) -> Self {
Self {
server: server.into(),
timeout: DEFAULT_TIMEOUT,
}
}
pub fn with_timeout(mut self, t: Duration) -> Self {
self.timeout = t;
self
}
}
#[async_trait]
impl Pinger for NtpPinger {
async fn ping(&self) -> Result<()> {
let endpoint = server_endpoint(&self.server)?;
let request = build_request();
with_timeout(self.timeout, async move {
let socket = UdpSocket::bind("0.0.0.0:0").await?;
socket.connect(&endpoint).await?;
socket.send(&request).await?;
let mut buf = [0u8; BUF_SIZE];
let n = socket.recv(&mut buf).await?;
validate_response(&buf[..n])
})
.await
}
}
fn server_endpoint(server: &str) -> Result<String> {
let uri = get_uri(server);
if uri.domain.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"NTP server target is missing a host",
));
}
let port = if uri.port > 0 {
uri.port as u16
} else {
DEFAULT_PORT
};
Ok(format!("{}:{}", uri.domain, port))
}
fn build_request() -> [u8; PACKET_LEN] {
let mut packet = [0u8; PACKET_LEN];
packet[0] = (NTP_VERSION << 3) | MODE_CLIENT;
packet
}
fn validate_response(buf: &[u8]) -> Result<()> {
if buf.len() != PACKET_LEN {
return Err(io::Error::other(format!(
"NTP response is {} bytes (expected {PACKET_LEN})",
buf.len()
)));
}
let first = buf[0];
let mode = first & 0x07;
if mode != MODE_SERVER && mode != MODE_BROADCAST {
return Err(io::Error::other(format!(
"NTP response mode {mode} is not server (4) or broadcast (5)"
)));
}
let version = (first >> 3) & 0x07;
if version != NTP_VERSION {
return Err(io::Error::other(format!(
"NTP response version {version} does not match request version {NTP_VERSION}"
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_request_packet_shape() {
let p = build_request();
assert_eq!(p.len(), PACKET_LEN);
assert_eq!(p[0], 0x23);
assert!(p[1..].iter().all(|&b| b == 0));
}
#[test]
fn validate_response_accepts_well_formed_server_reply() {
let mut buf = [0u8; PACKET_LEN];
buf[0] = (NTP_VERSION << 3) | MODE_SERVER;
validate_response(&buf).unwrap();
}
#[test]
fn validate_response_accepts_broadcast_mode() {
let mut buf = [0u8; PACKET_LEN];
buf[0] = (NTP_VERSION << 3) | MODE_BROADCAST;
validate_response(&buf).unwrap();
}
#[test]
fn validate_response_rejects_short_packet() {
let buf = [0u8; PACKET_LEN - 1];
assert!(validate_response(&buf).is_err());
}
#[test]
fn validate_response_rejects_long_packet() {
let buf = [0u8; PACKET_LEN + 1];
assert!(validate_response(&buf).is_err());
}
#[test]
fn validate_response_rejects_client_mode() {
let mut buf = [0u8; PACKET_LEN];
buf[0] = (NTP_VERSION << 3) | MODE_CLIENT;
assert!(validate_response(&buf).is_err());
}
#[test]
fn validate_response_rejects_version_mismatch() {
let mut buf = [0u8; PACKET_LEN];
buf[0] = (2 << 3) | MODE_SERVER;
assert!(validate_response(&buf).is_err());
}
#[test]
fn server_endpoint_applies_default_port() {
assert_eq!(
server_endpoint("time.example.com").unwrap(),
"time.example.com:123"
);
}
#[test]
fn server_endpoint_keeps_explicit_port() {
assert_eq!(
server_endpoint("time.example.com:1234").unwrap(),
"time.example.com:1234"
);
}
}