how to check whether the system time is synchronised to NTP server without knowing the ntp client used in the product?

Applications on general purpose compute cannot know in all cases how time sync works on the hosts they run on. In a container, you do not see and cannot connect to the chronyd or ntpd running on the host, yet this keeps time just fine. Or a VM guest that relies on host time sync, also not visible. Further making a general answer difficult, there are more NTP implementations than you might think: chrony, ntp, ntpsec, openntpd, w32tm.

Often documentation of the importance of correct time is sufficient.

On some platforms, making a dependency on an ntpd starting is relatively straight forward. On RHEL, to wait for time sync systemctl enable chrony-wait and add to your systemd unit

After=time-sync.target
Requires=time-sync.target

Yet there are applications with strict time requirements. Most demanding I can think of is time stamping authorities, one of which claims standards require less than one second offset or nothing can be issued. This aggressive of a response implies the application does its own time checks.

Perhaps bundling a SNTP client, which checks NTP offsets in your application, against configurable NTP servers. Cannot check for a proper ntpd running, but can sanity check offsets regardless of how time sync works to the host.


There is two ways to do this.

If the container you're running has a full systemd implementation, then the timedatectl program can inform you if the host is synchronized or not.

The way that this is internally managed is via dbus which speaks to the systemd-timedated daemon. What it is doing is perfoming a system call: adjtimex from which its possible to get data back indicating the current status of the adjustment in the kernel (if any) that is being done.

Hence, the second way to do this yourself without a full implementation is by using the adjtimex() system call.

The kernel doesn't want to have time jumps in its reporting of the time (or worse, time travelling backwards) as such it implements a skew in the time which over a the course of a few hours would correct a systems time (its done by adding or delaying a few milliseconds per second until the adjustment is complete).

The adjtimex system call is typically used by NTP systems to change the current skew the clock is facing to get it to synchronize correctly with a true clock source -- but it can also be used to fetch the current status of the skew of the clock source. Hence it gives you the ability to peek into the kernels idea of what sync is being done (if any).

The man page for adjtimex provides a couple of interesting portions that pertain to what you are asking:

       The  buf.status  field  is a bit mask that is used to set and/or retrieve status bits associated with the NTP implementation.  Some bits in the mask
       are both readable and settable, while others are read-only.
...
       STA_UNSYNC (read-write)
              Clock unsynchronized.

and

RETURN VALUE
       On success, adjtimex() and ntp_adjtime() return the clock state; that is, one of the following values:
...
       TIME_ERROR  The system clock is not synchronized to a reliable server.  This value is returned when any of the following holds true:

                   *  Either STA_UNSYNC or STA_CLOCKERR is set.

                   *  STA_PPSSIGNAL is clear and either STA_PPSFREQ or STA_PPSTIME is set.

                   *  STA_PPSTIME and STA_PPSJITTER are both set.

                   *  STA_PPSFREQ is set and either STA_PPSWANDER or STA_PPSJITTER is set.

                   The symbolic name TIME_BAD is a synonym for TIME_ERROR, provided for backward compatibility.

As such, if you dont have a fully fledged container, its possible to still get this data. I wrote a simple program that will fetch the status of the kernels skew via adjtimex in C. You can compile it for example gcc -o timex timex.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

#include <sys/timex.h>

/* Written for https://serverfault.com/questions/1077601/how-to-check-whether-the-system-time-is-synchronised-to-ntp-server-without-knowi */

void test_status(
    int st) 
{
  if (st & STA_PLL)
    printf("Phase locked loop\n");
  if (st & STA_PPSFREQ)
    printf("Pulse per second frequency discipline\n");
  if (st & STA_FLL)
    printf("PPS Time discipline\n");
  if (st & STA_INS)
    printf("Insert leap second and end-of-day\n");
  if (st & STA_DEL)
    printf("Delete leap second and end-of-day\n");
  if (st & STA_UNSYNC)
    printf("Clock is not syncronized\n");
  if (st & STA_FREQHOLD)
    printf("Hold frequency\n");
  if (st & STA_PPSSIGNAL)
    printf("Valid PPS signal is present\n");
  if (st & STA_PPSJITTER)
    printf("PPS signal jitter exceeded\n");
  if (st & STA_PPSWANDER)
    printf("PPS Signal wander exceeded\n");
  if (st & STA_PPSERROR)
    printf("PPS signal calibration error\n");
  if (st & STA_CLOCKERR)
    printf("Clock hardware fault\n");

  if (st & STA_NANO)
    printf("Nanosecond resolution\n");
  else
    printf("Microsecond resolution\n");

  if (st & STA_MODE)
    printf("Frequency locked loop\n");
  else
    printf("Phase locked loop\n");
}

int main() {
  struct timex tx = {};
  tx.modes = ADJ_OFFSET_SS_READ;
  int err = adjtimex(&tx);

  switch(err) {
    case -1:
      printf("Time error: %s\n", strerror(errno));
    break;

    case TIME_WAIT:
      printf("Leap second insert/delete completed\n");
    break;

    case TIME_INS:
      printf("Leap second to be added next UTC day\n");
    break;

    case TIME_DEL:
      printf("Leap second to be deleted next UTC day\n");
    break;

    case TIME_OOP:
      printf("Leap second insertion in progress\n");
    break;

    case TIME_ERROR:
      printf("Error getting time\n");
    break;

    case TIME_OK:
      printf("Time OK\n");
    break;

    default:
      printf("Time default: %x (%d)\n", err, err);
    break;
  }

  test_status(tx.status);
  exit(0);
}

Running on a system that is not syncronised:

$ ./timex 
Error getting time
Clock is not syncronized
Microsecond resolution
Phase locked loop

Running in a container on the same host that is not synchronised:

# podman run -v /tmp/timex/timex:/timex  docker.io/gammabytehosting/rockylinux /timex
Error getting time
Clock is not syncronized
Microsecond resolution
Phase locked loop

Setting the time in the host system to be synchronised:

# systemctl start chronyd
# chronyc sources
210 Number of sources = 9
MS Name/IP address         Stratum Poll Reach LastRx Last sample               
===============================================================================
^* _gateway            2   6     7     1  +5568ns[ -720ms] +/-   32ms
# ./timex 
Time OK
Microsecond resolution
Phase locked loop

Performing the same programmatic check in the container on the same host:

# podman run -v /tmp/timex/timex:/timex  docker.io/gammabytehosting/rockylinux /timex
Time OK
Microsecond resolution
Phase locked loop

There is potentially some issue with time namespaces that I haven't tested (they are really very new though) to see if they differ or honour adjtimex in a separate context (see man 7 time_namespaces) but from what I've read its probably still going to work -- I'd leave that for you to determine.