Implementing NTPv5 in ntpd-rs
What is NTP, and ntpd-rs?
The network time protocol (NTP) is a protocol to synchronize the clock of a machine over an unreliable network such as the internet. It has been around since 1985 (RFC958) and is used by all major operating systems, but also in many embedded devices.
Ntpd-rs is Tweede golf's modern, memory-safe implementation of NTP (with NTS) in Rust. The goal for ntpd-rs is to be a memory-safe alternative to projects like ntpd, NtpSec, and chrony.
Rust's rich type system also makes it easy to expand the project for newer protocol versions, which makes prepping for NTPv5 a lot easier.
Or if you want to know All The Details about our Rust implementations of NTP and PTP, you can find all relevant blogs here.
Why a new NTP version?
If NTP is working just fine, why would we need a new version of NTP at this time? NTP version 4 has some problems that are addressed in NTP version 5.
NTPv5 can express time more reliably in the future: NTPv4 uses a timestamp that will roll-over in 20361. In theory all clients should handle this gracefully. The NTP epoch starts in 1900 and all known NTP implementation were written after 1980, so they should detect the roll-over and continue on their merry way. NTPv5 pre-emptively fixes this by adding a new epoch field to the packet to extend the time range from 136 years to about 35000 years.
NTPv5 is more precise: It allows a mode which measures timestamps for packets sent more precisly. It also lets you specify the exact timescale (UTC, TAI, leap smeared UTC) to use. And it adds a new method to avoid two servers trying to synchronize to each other.
NTPv5 is more secure: It no longer requires implementation of error-prone protocol modes, and it explicitly no longer sends the current system time (which leaked security sensitive information).
NTPv5 is easier to extend: The extension field logic no longer has to check if an extension is the last one and add padding.
The answer to "Why now?" is this: If we want to change what NTP looks like in 15 years, we need to change it soon. NTP is a very slow moving protocol, in part because it is embedded in so many devices. Many routers, access-points, smart-lamps, and vacuum robots use NTP to get time, but they might never get a firmware upgrade. Also designing a new protocol takes a lot of time; it might well take another three to five years before NTPv5 is stabilized.
Why already implement a draft?
So if NTPv5 is not even stabilized yet, why are we already implementing it in ntpd-rs? The main reason to start now is so that we can provide feedback in the RFC process2. During implementation we already found some missing parts, like a way to tell a client to stop sending requests.
And since we integrate the new protocol version into our existing implementation, we can identify and address pain points for code duplication, as well as pain points for interpreting the specification. Some things look simple in prose but are too vague to implement in code. Pair programming has helped us to identify parts that could be implemented in different ways.
We can also identify problems in operating the new protocol. As I said there are a lot of old NTP implementations in the wild, and we can discover how they will interact with our implementation and see if it causes problems. We're then automatically testing all the parts between the client and the server, like routers, firewalls and network monitoring. For example some firewalls drop all packets on the NTP port 123 if they are not exactly 48 bytes long.3
This trial-and-error is proving useful to us, as it is hard if not impossible to know in advance how all the components will interact with this new protocol.
How did the implementation go?
Adding this new protocol to the existing code base was fairly painless. We wanted to ensure that users who do not want to try out a draft protocol would not have to pay for it in stability, binary size or performance. So we added a
feature flag that would cause any code related to NTPv5 to only compile when the user opts into it. (This is similar to
#ifdef statements in C.)
We also used
enums, which in Rust can contain data elements. So in any case where the behavior differs between NTPv4 and NTPv5 there is now an enum with a variant each for the two versions. The compiler ensures that we always check all cases and in the case where the
feature is not enabled, the compiler can trivially optimize out the enum which then only has a single variant.
In order to test interoperability and get more eyes on our implementation, we traveled to the IETF 118 in Prague to participate in the Hackathon and join the NTP working group meeting. We were able to finish our server implementation and chat with other participants about our work.
IETF 118 Hackathon in progress
How can I use it?
If you want to test out our implementation - even though it is a draft implementation of a draft specification - you can. In fact, we would appreciate the feedback! It is at the moment not packaged like the stable version - so you will need to build it manually like this:
git clone https://github.com/pendulum-project/ntpd-rs.git
cargo build --release --features unstable_ntpv5
Then install a stable ntpd-rs as per the documentation. And then replace the binaries with the unstable version you just built:
sudo cp ntp-ctl ntp-daemon ntp-metrics-exporter /usr/bin/
Currently ntpd-rs will always start out using NTPv4 but with the feature we will include a magic word into the packet to detect if a server supports NTPv5. If that is the case all further packets will use NTPv5 instead of NTPv4. To check that it actually is using NTPv5 you can look at the logs for the message
Received a valid upgrade response, switching to NTPv5!.
In case you encounter any problems with the new feature, please open an issue on the ntpd-rs github project. (This can also include issues like a server being unreachable - maybe because of a middle-box dropping the new "weird-looking" packets.)
Care to sponsor our work on time synchronisation?
The Pendulum project is building modern, full-featured implementations of the Network Time Protocol and the Precision Time Protocol. Pendulum focuses on security and robustness and uses the Rust programming language to guarantee memory safety.