# compliant Simple Network Time Protocol (SNTP) client
library for Rust.
`rsntp` provides an API to synchronize time with SNTPv4 time servers with the following features:
* Provides both a synchronous (blocking) and an (optional) asynchronous, `tokio` based API
* Optional support for time and date crates `chrono` and `time` (`chrono` is enabled by
default)
* IPv6 support
## Usage
Add this to your `Cargo.toml`:
```toml
[dependencies]
rsntp = "4.1.1"
```
Obtain the current local time with the blocking API:
```no_run
use rsntp::SntpClient;
use chrono::{DateTime, Local};
let client = SntpClient::new();
let result = client.synchronize("pool.ntp.org").unwrap();
let local_time: DateTime<Local> =
DateTime::from(result.datetime().into_chrono_datetime().unwrap());
println!("Current time is: {}", local_time);
```
You can also use the asynchronous API to do the same:
```no_run
use rsntp::AsyncSntpClient;
use chrono::{DateTime, Local, Utc};
async fn local_time() -> DateTime<Local> {
let client = AsyncSntpClient::new();
let result = client.synchronize("pool.ntp.org").await.unwrap();
DateTime::from(result.datetime().into_chrono_datetime().unwrap())
}
```
## API changes in version 3.0
Version 3.0 made core code independent of time and date crates and added support for the `time` crate.
This led to some breaking API changes, `SynchronizationResult` methods will return with wrappers
struct instead of `chrono` ones. Those wrapper structs has `TryInto` implementation and helper
methods to convert them to `chrono` format.
To convert old code, replace
```no_run
# use rsntp::SntpClient;
# let result = SntpClient::new().synchronize("pool.ntp.org").unwrap();
let datetime = result.datetime();
```
with
```no_run
# use rsntp::SntpClient;
# use chrono::{DateTime, Utc};
# let result = SntpClient::new().synchronize("pool.ntp.org").unwrap();
let datetime = result.datetime().into_chrono_datetime().unwrap();
```
or with
```no_run
# use rsntp::SntpClient;
# use chrono::{DateTime, Utc};
# let result = SntpClient::new().synchronize("pool.ntp.org").unwrap();
let datetime: chrono::DateTime<Utc> = result.datetime().try_into().unwrap();
```
The same applies to `Duration`s returned by `SynchronizationResult`.
## Support for time and date crates
`rsntp` supports returning time and date data in different formats. Currently the format of
the two most popular time and date handling crates supported: `chrono` and `time`.
By default, `chrono` is enabled, but you can add `time` support with a feature:
```no_run
use rsntp::SntpClient;
let client = SntpClient::new();
let result = client.synchronize("pool.ntp.org").unwrap();
let utc_time = result
.datetime()
.into_offset_date_time()
.unwrap();
println!("UTC time is: {}", utc_time);
```
Support for both crates can be enabled independently; you can even enable both
at the same time.
## Disabling asynchronous API
The asynchronous API is enabled by default, but you can disable it. Disabling it
has the advantage that it removes the dependency to `tokio`, which reduces
the amount of dependencies significantly.
```toml
[dependencies]
rsntp = { version = "4.1.1", default-features = false, features = ["chrono"] }
```
## System clock assumptions
`rsntp` assumes that system clock is monotonic and stable. This is especially important
with the `SynchronizationResult::datetime()` method, as `SynchronizationResult` stores just
an offset to the system clock. If the system clock is changed between synchronization
and the call to this method, then offset will not be valid anymore and some undefined result
might be returned.
## IPv6 support
`rsntp` supports IPv6, but for compatibility reasons, it binds its UDP socket to an
IPv4 address (0.0.0.0) by default. That might prevent synchronization with IPv6 servers.
To use IPv6, you need to set an IPv6 bind address:
```no_run
use rsntp::{Config, SntpClient};
use std::net::Ipv6Addr;
let config = Config::default().bind_address((Ipv6Addr::UNSPECIFIED, 0).into());
let client = SntpClient::with_config(config);
let result = client.synchronize("2.pool.ntp.org").unwrap();
let unix_timestamp_utc = result.datetime().unix_timestamp();
```
"##
)]
mod core_logic;
mod error;
mod packet;
mod result;
mod to_server_addrs;
pub use error::{ConversionError, KissCode, ProtocolError, SynchronizationError};
pub use packet::{LeapIndicator, ReferenceIdentifier};
pub use result::{SntpDateTime, SntpDuration, SynchronizationResult};
pub use to_server_addrs::ToServerAddrs;
use core_logic::{Reply, Request};
use packet::Packet;
use std::default::Default;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::time::Duration;
#[cfg(feature = "async")]
use tokio::time::timeout;
const SNTP_PORT: u16 = 123;
#[derive(Clone, Debug, Hash)]
pub struct Config {
bind_address: SocketAddr,
timeout: Duration,
connect_ip: bool,
}
impl Config {
pub fn bind_address(self, address: SocketAddr) -> Config {
Config {
bind_address: address,
..self
}
}
pub fn timeout(self, timeout: Duration) -> Config {
Config { timeout, ..self }
}
pub fn connect_ip(self, connect_ip: bool) -> Self {
Config { connect_ip, ..self }
}
}
impl Default for Config {
fn default() -> Config {
Config {
bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0),
timeout: Duration::from_secs(3),
connect_ip: true,
}
}
}
#[derive(Clone, Debug, Hash)]
pub struct SntpClient {
config: Config,
}
impl SntpClient {
pub fn new() -> SntpClient {
SntpClient {
config: Config::default(),
}
}
pub fn with_config(config: Config) -> SntpClient {
SntpClient { config }
}
pub fn synchronize<A: ToServerAddrs>(
&self,
server_address: A,
) -> Result<SynchronizationResult, SynchronizationError> {
let socket = std::net::UdpSocket::bind(self.config.bind_address)?;
let remote_address = server_address.to_server_addrs(SNTP_PORT);
socket.set_read_timeout(Some(self.config.timeout))?;
if self.config.connect_ip {
socket.connect(&remote_address)?;
}
let request = Request::new();
let mut receive_buffer = [0; Packet::ENCODED_LEN];
if self.config.connect_ip {
socket.send(&request.as_bytes())?;
} else {
socket.send_to(&request.as_bytes(), &remote_address)?;
}
let (bytes_received, server_address) = socket.recv_from(&mut receive_buffer)?;
let reply = Reply::new(
request,
Packet::from_bytes(&receive_buffer[..bytes_received], server_address)?,
);
reply.process()
}
pub fn set_timeout(&mut self, timeout: Duration) {
self.config.timeout = timeout;
}
pub fn set_bind_address(&mut self, address: SocketAddr) {
self.config.bind_address = address;
}
pub fn set_config(&mut self, config: Config) {
self.config = config
}
}
impl Default for SntpClient {
fn default() -> Self {
SntpClient::new()
}
}
#[cfg(feature = "async")]
pub struct AsyncSntpClient {
config: Config,
}
#[cfg(feature = "async")]
impl AsyncSntpClient {
pub fn new() -> AsyncSntpClient {
AsyncSntpClient {
config: Config::default(),
}
}
pub fn with_config(config: Config) -> AsyncSntpClient {
AsyncSntpClient { config }
}
pub async fn synchronize<A: ToServerAddrs>(
&self,
server_address: A,
) -> Result<SynchronizationResult, SynchronizationError> {
let mut receive_buffer = [0; Packet::ENCODED_LEN];
let remote_address = server_address.to_server_addrs(SNTP_PORT);
let socket = tokio::net::UdpSocket::bind(self.config.bind_address).await?;
if self.config.connect_ip {
socket.connect(&remote_address).await?;
}
let request = Request::new();
if self.config.connect_ip {
socket.send(&request.as_bytes()).await?;
} else {
socket.send_to(&request.as_bytes(), &remote_address).await?;
}
let result_future = timeout(self.config.timeout, socket.recv_from(&mut receive_buffer));
let (bytes_received, server_address) = result_future.await.map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::TimedOut,
"Timeout while waiting for server reply",
)
})??;
let reply = Reply::new(
request,
Packet::from_bytes(&receive_buffer[..bytes_received], server_address)?,
);
reply.process()
}
pub fn set_timeout(&mut self, timeout: Duration) {
self.config.timeout = timeout;
}
pub fn set_bind_address(&mut self, address: SocketAddr) {
self.config.bind_address = address;
}
pub fn set_config(&mut self, config: Config) {
self.config = config
}
}
#[cfg(feature = "async")]
impl Default for AsyncSntpClient {
fn default() -> Self {
AsyncSntpClient::new()
}
}