(2023-04-09) On reliable timekeeping on slow networks ----------------------------------------------------- I am very passionate about timekeeping. I have a nice collection of watches, some of which are capable of syncing via longwaves or even Bluetooth LE, and the protocol to do this already is reverse-engineered, at least to the extent of performing basic time synchronization tasks ([1]). I'll probably write a non-GUI tool for it as well once I figure out what the most optimal stack is to write it on top of. But, of course, these tools also need some source of truth. Something needs to set the time on our own client devices before we can pass it further or display it to the user. Nowadays, all time synchronization and coordination over the Internet is usually done via the NTP protocol. It's really well-engineered and takes into account a lot of factors and allows to receive accurate time all over the world. Can't really complain about that. One thing I can complain about though, is that it's too complex to reimplement from scratch, and some CVE reports about NTP server or client vulnerabilities just confirm that. On top of that, it involves a lot of overhead data in every synchronization packet, which might not be a lot in modern conventional networks, but pose a significant problem once we're talking about something like GPRS at 30 Kpbs, PSTN or CSD dialup at 9600 bps, AX.25 at 1200 bps or even slower transmission modes at 300 bps. In these conditions, every extra byte matters. The solution? Ye goode olde Time protocol (RFC 868), which some timekeeping networks (like time.nist.gov) still gracefully run on the port 37 of their servers. It returns a 4-byte (32-bit) timestamp on a TCP connection or as a response to any UDP datagram, and that's it. The timestamp is expected to represent the number of seconds since the beginning of the year 1900 UTC, and is supposed to roll over every 136 years. Now, I encourage you to only use the UDP mode of this protocol whenever you need it, as TCP connections made to just retrieve a 4-byte payload both pose significant overhead for our purposes and don't make server admins happy either. And, just like with NTP, you still need some way to measure elapsed time locally with around a millisecond precision. Once it is sorted though, the algorithm to get more or less accurate time (although with a whole second resolution) is very simple and straightforward: 1. Prepare a tool for time measurement of steps 2 and 3 combined. 2. Send a random 32-bit datagram to the Time server. 3. Receive the 32-bit timestamp datagram FTIME from the Time server. 4. Record the execution time (in milliseconds) of steps 2 and 3 as ETIME. 5. Obtain the true Unix timestamp (in seconds) using the following formula: TRUETIME = |(1000*FTIME + ETIME/2 - 2208988799500) / 1000| 6. Emit the TRUETIME value for further processing. End of algorithm. There is a couple of things that might need explanation here. First, the RFC says the Time protocol expects an empty datagram in UDP mode, but IRL most, if not all, implementations accept any datagram and just discard its contents. The 4-byte length of the outgoing datagram was chosen to make the resulting IP packet have exactly the same length as the one we're going to receive, so we can then safely divide the elapsed time by 2 to get more or less accurate correction. Second, the constant 2208988799500 is the number of milliseconds between the start of the year 1900 (where Time protocol timestamps start) and the start of the year 1970 (where the Unix epoch starts), minus 500 milliseconds used for rounding the final result properly. So, starting with the year 2036 when the 32-bit Time protocol counter rolls over, we will be adding 2082758400500 here instead of subtracting 2208988799500. And this is something that we need to know before applying the formula, but I hope no one will ever finds themselves in the situation they don't know whether or not the year 2036 already has come. But just in case, here is a more future-proof version of the same algorithm with a larger safety margin (until the year 2106): 1. Prepare a tool for time measurement of steps 2 and 3 combined. 2. Send a random 32-bit datagram to the Time server. 3. Receive the 32-bit timestamp datagram FTIME from the Time server. 4. Record the execution time (in milliseconds) of steps 2 and 3 as ETIME. 5. Obtain the true Unix timestamp (in seconds) using the following formula: TRUETIME = |(1000*FTIME + ETIME/2 - 2208988799500) / 1000| if FTIME > 2208988800, |(1000*FTIME + ETIME/2 + 2082758400500) / 1000| otherwise. 6. Emit the TRUETIME value for further processing. End of algorithm. Somewhere between the year 2036 and 2106, you can switch to using the second formula unconditionally, and this will prolong the algorithm to the year 2172. Afterwards, you adjust the offset accordingly (500 + the amount of milliseconds that actually passed between the start of 1970 and the start of 2172), and so on. This way, the 32-bit second counter can be reused forever. This approach has the following advantages: only a single send-receive round required, no transactional overhead thanks to UDP (4 bytes out, 4 bytes in), not having to rely on the local clock for anything but elapsed time measurement, and simple but accurate enough compensation for the roundtrip time. For really slow networks, I can't think of anything better at the moment. This is why I hope that even when NIST stops serving time using this protocol, someone still will. Maybe, I'll set it up right here on hoi.st, who knows. --- Luxferre --- [1]: https://git.sr.ht/~luxferre/RCVD