Target practice: ntpd-rs on FreeBSD and other platforms

Folkert
Systems software engineer
Target practice: ntpd-rs on FreeBSD and other platforms
The latest release of ntpd-rs compiles on several new targets: the FreeBSD and macOS operating systems now work, and ntpd-rs now supports musl libc on Linux. The PRs adding support for these platforms are all community contributions, which is very exciting.

Most of our code is written in a target-agnostic way. For most code, any unix-like target will do. But because our lowest layer interfaces directly with libc, some subtle changes may be needed to support a new target. For instance, some types have slightly different fields, or more modern Linux APIs don't have an equivalent.

Sockets

We use libc and usafe code to configure UDP sockets, so that they provide timestamps when sending or receiving messages. Linux provides several APIs for timestamping and we use the most modern and accurate method. The other unix-like systems don't have a direct equivalent.

Memory Layout

Musl makes some minor changes to memory layout of some types. It adds private padding fields, so it is impossible to initialize a value of those types using only safe code. We have to use a bit of unsafe code instead:

fn empty_msghdr() -> libc::msghdr {
    unsafe { MaybeUninit::<libc::msghdr>::zeroed().assume_init() }
}

Initializing with zeros should be fine for all libc types, but it certainly is for the particular ones that we use. In fact we must zero the memory here; just using MaybeUninit::uninit would trigger undefined behavior (UB), because using uninitialized memory as an integer/pointer is undefined behavior. This might be slightly unintuitive because the bits form a valid integer value. Nonetheless, the decision was made to declare this UB.

Timestamping Method

MacOS does not provide a method for nanosecond timestamps, it only supports the older posix method of microsecond timestamps, which are sufficiently accurate for NTP clients. MacOS systems are not designed to be (NTP) servers.

FreeBSD has its own nanosecond precision API, which is sligtly different from the one on Linux. An extra flag can be set, and from then on the timestamps will contain nanoseconds instead of microseconds.

Clock steering

FreeBSD and macOS use an older API to update the clock. Fortunately, Linux still supports this API, so the implementation also works on Linux, and it was easy to develop on Linux.

The major downside of these APIs is that many operations require more syscalls. For instance stepping the clock on Linux can take a delta, but on macOS and FreeBSD requires the full timestamp. That means that before setting the clock, you need to first read the clock to get the current time, then add the delta, then set the clock again.

Time elapses between reading and subsequently setting the clock, which reduces precision. The modern Linux APIs use just one atomic update to modify the current time and are therefore more precise.

Pool deployment

The code changes to support these 3 new platforms were relatively straightforward. But (sadly) the fact that ntpd-rs compiles for these targets does not guarantee that it actually works. We need to validate the behavior by actually running the code on the target system.

We now run our tests for all targets, to make sure all primitives work as expected. Then we also perform manual tests where we set the time forward or backward by some amount and observe that the clock is re-synchronized to the correct time.

Finally, to get some production experience and verify that our implementation performs well for real traffic, we added a FreeBSD server to the NTP pool.

Building for FreeBSD

I develop on Linux, and up to this point had not actually used FreeBSD. All of our CI machines are also Linux machines. But to deploy to the pool, we'd need a FreeBSD build of ntpd-rs. We also want this build to be reproducable.

Rust supports many targets, but the cross-compilation story is still rough. A major pain point is that there is no convenient way to acquire a linker and system libraries for the target platform.

Zig does much better: it bundles many linkers and system library headers into its binary, and it is my prefered way to do cross-compilation. cargo-zigbuild is a wrapper around cargo that uses zig as the linker. We use cargo-zigbuild in our CI to run clippy for some targets like 32-bit Linux for the raspberry pi.

However, FreeBSD is not currently supported by cargo-zigbuild, and I ran into missing libraries trying to make it work. I'm sure it's possible, but I have no patience for this.

So instead we use an inelegant Vagrant setup to spin up a FreeBSD VM, install Rust, and build our source code. I'm not proud of it, it still took a day to set up, but it works and the build is now reproducable.

Configuration

Next I set up a FreeBSD machine in Google Cloud. Then it is just a matter of putting the right files in the right places. We use an Ansible Playbook to move all of the files over.

The final missing ingredient is how to run ntpd-rs as a daemon. On Linux we use systemd to take care of the details, but FreeBSD does not have a direct equivalent. We eventually got a working /etc/rc.d/ntp-deamon configuration that has all of the behavior we want (like running in the background and rotating log files).

With our daemon set up, it was time to add our new server to the NTP pool and slowly watch its accuracy score climb.

ntpd-rs on FreeBSD slowly reaching a score of 20 in the pool ntpd-rs on FreeBSD slowly reaching a score of 20 in the pool

It looks like the output is a bit noisier than for our Linux servers, but it is more than adequate for the NTP pool.

Conclusion

It is very exciting that we support so many targets now. The FreeBSD compilation process is rough, but overall the process went smoothly. We are very happy to see such substantial community contributions!

Try ntpd-rs today. For Linux we provide installers. For FreeBSD this PR contains useful information.

We would greatly appreciate feedback from users, particlarly those who are running ntpd-rs on a sizable network of computers. Opening an issue to share feedback or suggestions is an option, but you may also contact Erik directly with your issues or proposed contributions.

Stay up-to-date

Stay up-to-date with our work and blog posts?

Related articles

In Dutch we have a saying 'meten is weten', which translates to 'to measure is to know'. That sentiment is frequently overlooked in setting up computers and networks.
Sovereign Tech Fund will support our effort to build modern and memory-safe implementations of the Network Time Protocol (NTP) and the Precision Time Protocol (PTP).
At RustNL 2023, a Rust conference held in Amsterdam recently, I had the opportunity to talk about ntpd-rs, our project implementing the Network Time Protocol.