#![cfg(feature = "tokio")]
mod common;
use std::time::Duration;
use ntp_server::protocol::{self, ConstPackedSizeBytes, Mode, Packet, Stratum, ToBytes, Version};
use ntp_server::server::NtpServer;
use ntp_server::server_common::{IpNet, RateLimitConfig};
use common::{
build_client_packet, build_client_packet_version, parse_response, send_receive_raw,
spawn_test_server,
};
#[tokio::test]
async fn test_server_binds_and_responds() {
let addr = spawn_test_server(NtpServer::builder()).await;
let request = build_client_packet();
let resp = send_receive_raw(addr, &request, Duration::from_secs(2))
.await
.expect("no response from server");
assert_eq!(resp.len(), Packet::PACKED_SIZE_BYTES);
let pkt = parse_response(&resp);
assert_eq!(pkt.mode, Mode::Server);
assert_eq!(pkt.version, Version::V4);
}
#[tokio::test]
async fn test_roundtrip_with_client_library() {
let addr = spawn_test_server(NtpServer::builder().stratum(Stratum(2))).await;
let result = ntp_client::async_ntp::request_with_timeout(addr, Duration::from_secs(2))
.await
.expect("client request failed");
assert_eq!(result.stratum, Stratum(2));
assert_eq!(result.mode, Mode::Server);
assert_eq!(result.version, Version::V4);
}
#[tokio::test]
async fn test_response_stratum_and_reference() {
let ref_id = protocol::ReferenceIdentifier::PrimarySource(protocol::PrimarySource::Gps);
let addr = spawn_test_server(
NtpServer::builder()
.stratum(Stratum(1))
.reference_id(ref_id),
)
.await;
let request = build_client_packet();
let resp = send_receive_raw(addr, &request, Duration::from_secs(2))
.await
.expect("no response");
let pkt = parse_response(&resp);
assert_eq!(pkt.stratum, Stratum(1));
assert_eq!(pkt.reference_id.as_bytes(), *b"GPS\0");
}
#[tokio::test]
async fn test_deny_list_drops_silently() {
let addr =
spawn_test_server(NtpServer::builder().deny(IpNet::new("::1".parse().unwrap(), 128))).await;
let request = build_client_packet();
let resp = send_receive_raw(addr, &request, Duration::from_millis(500)).await;
assert!(resp.is_none(), "denied client must receive no response");
}
#[tokio::test]
async fn test_allow_list_drops_unmatched() {
let addr =
spawn_test_server(NtpServer::builder().allow(IpNet::new("192.0.2.0".parse().unwrap(), 24)))
.await;
let request = build_client_packet();
let resp = send_receive_raw(addr, &request, Duration::from_millis(500)).await;
assert!(resp.is_none(), "restricted client must receive no response");
}
#[tokio::test]
async fn test_rate_limit_returns_kod_rate() {
let config = RateLimitConfig {
max_requests_per_window: 1,
window_duration: Duration::from_secs(10),
min_interval: Duration::ZERO,
};
let addr = spawn_test_server(NtpServer::builder().rate_limit(config)).await;
let request = build_client_packet();
let resp1 = send_receive_raw(addr, &request, Duration::from_secs(2))
.await
.expect("no response to first request");
let pkt1 = parse_response(&resp1);
assert_eq!(pkt1.mode, Mode::Server);
assert!(pkt1.stratum.0 > 0);
let resp2 = send_receive_raw(addr, &request, Duration::from_secs(2))
.await
.expect("no response to second request");
let pkt2 = parse_response(&resp2);
assert_eq!(pkt2.stratum, Stratum::UNSPECIFIED);
assert_eq!(pkt2.reference_id.as_bytes(), *b"RATE");
}
#[tokio::test]
async fn test_rate_limit_allows_after_window() {
let config = RateLimitConfig {
max_requests_per_window: 1,
window_duration: Duration::from_millis(100),
min_interval: Duration::ZERO,
};
let addr = spawn_test_server(NtpServer::builder().rate_limit(config)).await;
let request = build_client_packet();
let resp1 = send_receive_raw(addr, &request, Duration::from_secs(2))
.await
.expect("no response to first request");
let pkt1 = parse_response(&resp1);
assert_eq!(pkt1.mode, Mode::Server);
tokio::time::sleep(Duration::from_millis(150)).await;
let resp2 = send_receive_raw(addr, &request, Duration::from_secs(2))
.await
.expect("no response after window");
let pkt2 = parse_response(&resp2);
assert_eq!(pkt2.mode, Mode::Server);
assert!(pkt2.stratum.0 > 0);
}
#[tokio::test]
async fn test_invalid_packet_dropped() {
let addr = spawn_test_server(NtpServer::builder()).await;
let garbage = [0u8; 10];
let resp = send_receive_raw(addr, &garbage, Duration::from_millis(500)).await;
assert!(resp.is_none(), "server should not respond to garbage");
}
#[tokio::test]
async fn test_v3_request_gets_v3_response() {
let addr = spawn_test_server(NtpServer::builder()).await;
let request = build_client_packet_version(Version::V3);
let resp = send_receive_raw(addr, &request, Duration::from_secs(2))
.await
.expect("no response");
let pkt = parse_response(&resp);
assert_eq!(pkt.mode, Mode::Server);
assert_eq!(pkt.version, Version::V3);
}
#[tokio::test]
async fn test_interleaved_mode() {
let addr = spawn_test_server(NtpServer::builder().enable_interleaved(true)).await;
let request = build_client_packet();
let resp1 = send_receive_raw(addr, &request, Duration::from_secs(2))
.await
.expect("no response to first request");
let pkt1 = parse_response(&resp1);
assert_eq!(pkt1.mode, Mode::Server);
let resp2 = send_receive_raw(addr, &request, Duration::from_secs(2))
.await
.expect("no response to second request");
let pkt2 = parse_response(&resp2);
assert_eq!(pkt2.mode, Mode::Server);
assert_eq!(pkt2.origin_timestamp.seconds, 0xE0000000);
assert_eq!(pkt2.origin_timestamp.fraction, 0x12345678);
}
#[tokio::test]
async fn test_multiple_concurrent_clients() {
let addr = spawn_test_server(NtpServer::builder()).await;
let mut handles = Vec::new();
for i in 0u32..10 {
let handle = tokio::spawn(async move {
let packet = {
let pkt = Packet {
transmit_timestamp: protocol::TimestampFormat {
seconds: 0xE0000000 + i,
fraction: i,
},
..Packet::default()
};
let mut buf = [0u8; Packet::PACKED_SIZE_BYTES];
pkt.to_bytes(&mut buf[..]).unwrap();
buf
};
let resp = send_receive_raw(addr, &packet, Duration::from_secs(5))
.await
.unwrap_or_else(|| panic!("client {i} got no response"));
let pkt = parse_response(&resp);
assert_eq!(pkt.mode, Mode::Server);
assert_eq!(pkt.origin_timestamp.seconds, 0xE0000000 + i);
});
handles.push(handle);
}
for h in handles {
h.await.expect("client task panicked");
}
}
#[tokio::test]
async fn test_origin_timestamp_echo() {
let addr = spawn_test_server(NtpServer::builder()).await;
let xmt_s = 0xDEADBEEF_u32;
let xmt_f = 0xCAFEBABE_u32;
let packet = {
let pkt = Packet {
transmit_timestamp: protocol::TimestampFormat {
seconds: xmt_s,
fraction: xmt_f,
},
..Packet::default()
};
let mut buf = [0u8; Packet::PACKED_SIZE_BYTES];
pkt.to_bytes(&mut buf[..]).unwrap();
buf
};
let resp = send_receive_raw(addr, &packet, Duration::from_secs(2))
.await
.expect("no response");
let pkt = parse_response(&resp);
assert_eq!(pkt.origin_timestamp.seconds, xmt_s);
assert_eq!(pkt.origin_timestamp.fraction, xmt_f);
}