kotfu.net

OpenBSD NTP Server With GPS Time Sensor

For a long time I’ve run redundant OpenBSD firewalls on my home network. These firewalls also provide redundant network services like DNS resolution and network time service. The reference implementation of the NTP protocol is often referred to as ntp. The implementation of the NTP protocol that ships with OpenBSD is called OpenNTPD. Like most daemons in OpenBSD, it is not a port of the reference implementation. It’s a rewrite from the ground up with the objective of making a cleaner, simpler, more secure, and easier to maintain codebase. To accomplish this goal, the authors chose to leave out many advanced features, which makes it less accurate than ntp and chrony. OpenNTPD won’t get you microsecond accuracy, but when compiled on Linux the binary for OpenNTPD is one tenth the size of the binary from the reference implementation.

For the vast majority of use cases, you need a three line configuration in ntpd.conf:

listen 127.0.0.1
listen 192.168.1.5
servers pool.ntp.org

Your clock will be synchronized with a random set of 4 internet time servers, and you will provide time services to other computers on your network. You can harden the configuration by adding an HTTP based constraint, which mitigates the risk rogue time servers or man-in-the -middle attacks on your time service:

constraint "https://9.9.9.9"

or

constraint "https://www.google.com"

It’s always bothered me a bit that I’m dependent on the internet for accurate time. I configured my Synology to use two factor authentication using TOTP, which depends on accurate time. If my internet connection is down, there is some very small chance I might not be able to log in. The chances of this problem happening to me are very low. But it is a fun problem to work on. I recently added a GPS time source to each of my firewalls, removing the dependency on the internet for accurate time. It’s easy and relatively inexpensive to do.

Hardware Considerations

GPS receivers work by receiving high precision time signals from several satellites and using the differences in time to calculate the location. USB GPS receivers are inexpensive. There are several options for less than $20. I’m not including Amazon links because apparently there is a lot of cheap counterfeit devices out there, and I’m not smart enough to tell the difference between the good ones and the junk. I bought a “G72” model that’s a small white USB dongle with a USB-A port.

We need a device that works with OpenBSD and returns accurate time and location data. The “G72” works fine, and others report that the “VK172” works. Both of these devices claim to have chips in them from u-blox. Maybe they do. I dunno, but the ones I bought can lock onto a signal and spit out the time and location.

Receiving GPS Signals

The GPS device needs to be able to receive radio transmissions from satellites. Some have reported the GPS receiver needs to be near a window or be outside with a clear view of the sky in order to receive those transmissions. In my experiments, that’s not the case. I have my GPS receiver in a rack in a windowless basement room and it receives signals just fine. But it must be oriented the correct way. The little dongle has a patch antenna inside, and that antenna must be facing up. My dongles (I bought two because I have two firewalls and each one provides time service) have a green LED light that blinks once when you first plug the dongle in. It also blinks every second once it’s locked on to a signal. On my device, the side with the LED is the side with the patch antenna. LED up, antenna up. On my firewalls, the USB ports are oriented so that the light on the dongle is facing down. Digging in my box of extra cables I found some USB-A extensions which allowed me to position the dongles on top of the rack and in the proper orientation.

The first time you power up one of these GPS receivers, it has to acquire the GPS almanac from the satellite cloud. For this first run activity, maybe it’s useful to have the USB device in a place with a reasonably good view of the sky (like outdoors or near a window). One less thing that could go wrong. You don’t have to move your OpenBSD machine to that place. Any powered USB port will do, it could be on a laptop or a battery bank. You don’t need any software either. Plug in the USB device, making sure it’s oriented the correct way, and let it sit for a few minutes. Eventually the green light will start blinking, which tells you it has received the almanac and acquired a location fix. Some people report it taking 30 minutes to get an initial position fix. That seems unusually long and probably means the antenna is not able to receive a good signal from the satellites.

After that first run, my USB GPS receivers can acquire a signal and start the blinking green light within a couple of seconds of being plugged in.

Set Up the GPS Receiver As An OpenBSD Sensor

When I plug the USB key into my OpenBSD machine, the kernel detects:

# dmesg | grep umodem
umodem0 at uhub1 port 6 configuration 1 interface 0 "u-blox AG - www.u-blox.com u-blox GNSS receiver" rev 1.10/3.01 addr 2
umodem0: data interface 1, has CM over data, has no break
umodem0: status change notification available
ucom0 at umodem0: usb1.0.00006.1

Most GPS receivers share location, time, and speed data using the NMEA 0183 format. It’s proprietary and I would have to pay $2,000 to buy the specification. Eric S. Raymond hates that the format is proprietary, so he reverse engineered, and documented it for anyone to use. The format is plain text sent over serial connections (hardwired serial port, USB serial, or even Bluetooth serial). Each packet of information is called a sentence, and is contained on a single like that starts with a $ and ends with CR/LF.

If the USB device is recognized as shown above, you can monitor the NMEA sentences sent by the GPS receiver. To do that we’ll use the ancient program cu, which was originally written to Call Up another system on a modem. cu connects your terminal session to a serial port, which in the olden days would have had a physical modem connected to it, and from there you could issue the commands to the modem to have it dial a phone number and connect to another system. cu is installed as part of the base OpenBSD operating system.

The kernel will create a tty for us when the USB GPS receiver is connected. Usually that tty device is /dev/cuaU0. All the rules of serial ports apply, you need the right stop bits, baud rate, etc. These USB devices use 8N1, which is the most common serial line configuration, and they just work without having to change or configure anything. As far as I can tell, they automatically adapt to whatever baud rate you use. I choose the fastest common baud rate of 115200. When you are synchronizing time with microsecond resolution, the delay of transmitting at 9600 baud is meaningful.

Type:

$ doas cu -l /dev/cuaU0 -s 115200

and you’ll see streams of text flowing by. To exit cu, type ~..

If you monitor the serial port right after you plug in the GPS USB receiver, you might see something like this:

$GNTXT,01,01,02,u-blox AG - www.u-blox.com*4E
$GNTXT,01,01,02,HW UBX-M8130 00080000*61
$GNTXT,01,01,02,ROM CORE 3.01 (107888)*2B
$GNTXT,01,01,02,FWVER=SPG 3.01*46
$GNTXT,01,01,02,PROTVER=18.00*11
$GNTXT,01,01,02,GPS;GLO;BDS*06
$GNTXT,01,01,02,QZSS*58
$GNTXT,01,01,02,GNSS OTP=GPS;BDS*26
$GNTXT,01,01,02,LLC=FFFFFFFF-FFFFFFFF-FFFFFFFF-FFFFFFFF-FFFFFFFD*2F
$GNTXT,01,01,02,ANTSUPERV=AC SD PDoS SR*3E
$GNTXT,01,01,02,ANTSTATUS=OK*25
$GNTXT,01,01,02,PF=3FF*4B

The ublox protocol specification says that these TXT sentences “output[s] various information on the receiver, such as power-up screen, software version etc.”

OpenBSD has the ability to process incoming serial data directly from a tty. Because this data could come in multiple formats, you have to tell it which format to process. For reasons lost in the mists of time, instead of calling it a “format” the kernel calls it a “line discipline”. We’ll use ldattach to tell the kernel to process NMEA formatted data on /dev/cua0:

$ doas /sbin/ldattach -s 115200 nmea /dev/cuaU0

Give it a couple seconds, and if it’s all working correctly, the kernel will show some new sensors:

$ sysctl hw.sensors
hw.sensors.nmea0.indicator0=On (Signal), OK
hw.sensors.nmea0.timedelta0=0.026972 secs (GPS autonomous), OK, Thu Aug  1 20:44:27.026
hw.sensors.nmea0.angle0=xxx.5093 degrees (Latitude), OK
hw.sensors.nmea0.angle1=xxx.8564 degrees (Longitude), OK
hw.sensors.nmea0.distance0=1382.500 m (Altitude), OK
hw.sensors.nmea0.velocity0=0.073 m/s (Ground speed), OK

Obvs I masked my location because creepers exist.

If your GPS receiver hasn’t acquired a position you might see something like this:

$ sysctl hw.sensors
hw.sensors.nmea0.indicator0=Off (Signal), CRITICAL
hw.sensors.nmea0.distance0=0.000 m (Altitude), WARNING

Reposition the GPS receiver and try again.

If you don’t see any nmea0 sensors in the output, then OpenBSD isn’t receiving any valid NMEA sentences. You probably have the wrong device in your ldattach command.

The ldattach process forks into the background and continues running. If you unplug and replug the USB GPS receiver, you’ll need to kill the background process and re-run it. It doesn’t seem to be able to re-attach on its own.

Make The Configuration Permanent

As currently configured, your sensor will go away the next time you reboot your machine. We need to have a way to run that ldattach command every time the computer boots. To do so, we add the following to /etc/ttys:

# gps sensor
cuaU0   "/sbin/ldattach -s 115200 nmea" unknown on  softcar

This file tells init which processes to start on which ttys when the system boots. Usually it starts getty so a user can login. In this case, we’ll start ldattach instead.

Make sure you don’t already have an ldattach process running, then type:

$ doas kill -s HUP 1

That will tell init, which is always PID 1, to reread /etc/ttys and it will start up the ldattach process for us. Give it a few seconds to catch up and then the sensors should show up when you do sysctl hw.sensors.

The OpenBSD man pages are comprehensive and accurate. The man page for ldattach says that you should use the cua devices when starting it from the command line and the tty devices when running it from init. I tried using /dev/ttyU0 in /etc/ttys and it didn’t work. But it works fine using /dev/cuaU0.

Configuring ntpd

Configure ntpd to use this new sensor by adding the following to ntpd.conf:

sensor nmea0 refid GPS trusted weight 2

There is a common misconception that you can set the stratum of your time server, or that stratum is an indication of quality or reliability of the time. Stratum is the number of hops between you and a reference clock. You choose your time sources (network or sensor), and each source tells you what stratum it is. Your stratum is 1 higher that the source that you are currently synchronized with. That source you are synchronized with can change at any time, meaning the stratum of your server can change at any time.

OpenBSD lets you choose the stratum of your time sensor, but it defaults to 1. The GPS Satellite is stratum 0 because it contains an atomic clock. Our server is 1 hop away from that reference clock because we have a GPS receiver. We’ll use the OpenBSD default stratum of 1.

I use the trusted keyword to skip the constraint checks I have in my ntpd.conf file. If my internet connection is down, trusted lets me sync to the GPS sensor without cross-checking it with an HTTPS server.

I don’t fully understand how weights work in OpenNTPD, but it seems to me that a local sensor should have more weight. I set it at 2. Others recommend setting the weight at 5. Maybe you will try something else, like 3 or 7.

Time Correction

With a mix of network sources and sensors, you may notice a big difference in the offset between the two source types. For network sources, the system can measure the network delay to and from a network time server, and incorporate that into the calculations for the local time. For a sensor, the system has no way to make such a measurement. But there is certainly a delay between the time the radio waves from a GPS satellite are processed by a GPS chip, transmitted over a USB connection which acts like a serial port, and processed by the other side. The reason we attached to the sensor at 115200 baud instead of the standard 9600 baud, is because at 9600 baud each byte sent over the wire takes 1 millisecond. A 60 character NMEA sentence would incur a 60 millisecond delay in transit, plus whatever processing time is required on each side. I expect these delays will vary widely based on chipset and CPU performance.

Here’s what it looks like on one of my servers:

4/4 peers valid, 1/1 sensors valid, constraint offset -1s, clock synced, stratum 3

peer
   wt tl st  next  poll          offset       delay      jitter
162.159.200.1 time.cloudflare.com
    1 10  3    2s   33s         0.334ms     2.930ms     0.316ms
208.67.75.242 0.pool.ntp.org
    1 10  3   31s   31s        -2.811ms    58.286ms     0.387ms
198.169.208.141 1.pool.ntp.org
    1 10  3    9s   33s         1.318ms    23.410ms     0.389ms
173.255.230.96 2.pool.ntp.org
 *  1 10  2   10s   34s         0.168ms    53.765ms     0.405ms

sensor
   wt gd st  next  poll          offset  correction
nmea0  GPS
    2  1  0    6s   15s       -28.100ms     0.000ms

Notice the 28 millisecond offset from our sensor. That’s way higher than the offset from the network sources. This difference is caused by the delays in transmission and processing on our GPS sensor, the USB cable, and in our OpenBSD server. Maybe you don’t care, but it bothers me. It’s easy to fix. In your ntpd.conf file change this line:

sensor nmea0 refid GPS trusted weight 2

to look like this:

sensor nmea0 refid GPS correction 30000 trusted weight 2

Here we add a 30000 microsecond (or 30 millisecond) adjustment to the time received from the GPS sensor to correct for processing and transmission. You’ll have to watch your offset and choose a correction value that matches the offset you typically see on the sensor when ntpd is synchronized with a network source. With the correction applied, I get:

4/4 peers valid, 1/1 sensors valid, constraint offset -1s, clock synced, stratum 1

peer
   wt tl st  next  poll          offset       delay      jitter
162.159.200.1 time.cloudflare.com
    1 10  3   16s   33s        -2.675ms     2.495ms     0.140ms
23.141.40.123 0.pool.ntp.org
    1 10  2    7s   31s        14.696ms    11.705ms     0.140ms
162.159.200.123 1.pool.ntp.org
    1 10  3   32s   33s        -2.656ms     2.460ms     0.195ms
167.248.62.201 2.pool.ntp.org
    1 10  2    2s   33s         4.776ms    40.685ms     0.559ms

sensor
   wt gd st  next  poll          offset  correction
nmea0  GPS
 *  2  1  0   10s   15s         1.382ms    30.000ms

That’s close enough for me.

Changelog

3 Aug 2024

  • Initial version, tested and validated on OpenBSD 7.5

References