Tech & GadgetsAI-generated

Hoe je klok op de seconde gelijk loopt: NTP

Elke computer houdt zelf de tijd bij met een klein kristal, en geen enkel kristal loopt precies goed. Op de Linux-server waarop ik dit uitzocht, corrigeert de kernel de kloksnelheid met 17,121 ppm, ofwel miljoenste delen. Dat lijkt niets, maar zonder die correctie is het 1,48 seconde per dag. Na drie weken zit je er dan een halve minuut naast, en bij grotere afwijkingen gaan dingen stuk die je niet met tijd in verband brengt, zoals HTTPS.

Wat de klok rechttrekt, is NTP, het Network Time Protocol. Hieronder zie je hoe je met vier tijdstempels de afwijking van je klok uitrekent zonder te weten hoe lang het pakketje onderweg was, en waarom een certificaat ongeldig wordt als je klok verkeerd staat. Alle metingen zijn gedaan op 15 oktober 2026 vanaf een Nederlandse verbinding, met een eigen NTP-client van een paar regels Python.

Eén vraag van 48 bytes

NTP draait over UDP, poort 123. Een vraag is een pakketje van 48 bytes. Voor een simpele vraag zet je het eerste byte op 0x23: versie 4, mode 3 ("client"). De rest mag leeg blijven.

Het antwoord is ook 48 bytes, met onder meer het stratum van de server, een referentie-ID en tijdstempels. Volgens RFC 5905, de specificatie van NTP versie 4 uit 2010, is een tijdstempel 32 bits seconden sinds 1 januari 1900 plus 32 bits fractie, goed voor een resolutie van 232 picoseconden. Die seconden lopen over op 7 februari 2036, 06:28:16 UTC.

Vier tijdstempels

NTP hoeft niet te weten hoe lang een pakketje onderweg is. Het meet dat mee, met vier momenten:

  • T1: de client verstuurt de vraag (klok van de client)
  • T2: de server ontvangt de vraag (klok van de server)
  • T3: de server verstuurt het antwoord (klok van de server)
  • T4: de client ontvangt het antwoord (klok van de client)

Een meting naar ntp1.time.nl (alleen de laatste cijfers):

TijdstempelWaarde (s)
T1...042,400874
T2...042,410291
T3...042,410375
T4...042,420054

RFC 5905 geeft twee formules. De round-trip delay is de totale tijd min de tijd die de server erover deed: (T4 − T1) − (T3 − T2). Hier is dat 19,180 − 0,084 = 19,096 ms. De offset, hoeveel de klok van de server voorloopt op die van mij, is het gemiddelde van twee verschillen: ((T2 − T1) + (T3 − T4)) / 2. Dat is (9,417 − 9,679) / 2 = −0,131 ms. Mijn klok liep dus 0,13 milliseconde voor op die van de server.

Waarom werkt dat? In T2 − T1 zit de reistijd heen plus de offset. In T3 − T4 zit de offset min de reistijd terug. Tel je ze op, dan vallen de reistijden tegen elkaar weg, mits heen en terug even lang duren.

Als heen en terug niet even lang zijn

Dat is de zwakke plek. NTP kan niet zien of het pakketje heen 5 ms deed en terug 15 ms. Elk verschil tussen heen- en terugweg komt voor de helft in de offset terecht. De echte offset ligt daardoor altijd binnen een halve delay van de berekende waarde, maar waar precies, weet je niet.

Ik liep daar zelf tegenaan. Mijn eerste script las T1 af vóór de DNS-opzoeking van de servernaam, die zo meetelde als "heenweg". Voor pool.ntp.org gaf dat een delay van 676,9 ms en een offset van 327,9 ms: bijna precies de helft. Volgens die meting liep mijn klok een derde seconde achter. Nadat ik de opzoeking vóór T1 zette, bleven alle gewone antwoorden binnen 5,1 ms.

Stratum: wie heeft zijn tijd van wie

Waar een server zijn tijd vandaan haalt, zie je aan het stratum. Stratum 1 is een server met een eigen referentieklok, zoals een GPS-ontvanger of een ontvanger voor het signaal van een tijdzender als DCF77. Een server die zijn tijd via NTP van een stratum 1 haalt, wordt stratum 2, enzovoort. Volgens de RFC gaat dat door tot 15; stratum 16 betekent "niet gesynchroniseerd".

Bij stratum 1 bevat de referentie-ID vier ASCII-tekens die de bron aanduiden. Bij hogere strata is het het IPv4-adres van de server waar de tijd vandaan komt. Wat ik terugkreeg:

ServerStratumReferentie-IDDelay
time.google.com1GOOG17,6–21,8 ms
ntp1.time.nl1TMNL18,3–21,5 ms
time.apple.com1PTPs21,4–26,0 ms
ptbtime1.ptb.de1PTB25,3–31,9 ms
time.cloudflare.com310.x.8.412,1–13,9 ms

Cloudflare is het snelst bereikbaar, maar zit twee stappen verder van een referentieklok, achter een interne server met een privé-adres.

Deze machine gebruikt systemd-timesyncd met een stratum 2-server uit debian.pool.ntp.org. Volgens de RFC is je eigen stratum dat van je server plus één, dus 3.

Bijsturen in plaats van springen

Veel software rekent op een klok die netjes doorloopt. De referentie-implementatie in RFC 5905 springt daarom bij een offset onder de 125 ms niet, maar laat de klok iets sneller of trager lopen tot het verschil weg is. Blijft het verschil langer dan 900 seconden boven de 125 ms, dan volgt een stap. Boven de 1000 seconden hoort het programma te stoppen met een melding.

Tegelijk leert de client hoeveel zijn kristal afwijkt: de 17,121 ppm uit de intro, die timedatectl timesync-status als Frequency toont. systemd-timesyncd begint met een vraag per 32 seconden en schaalt op tot eens per 2048 seconden. Op deze machine stond hij al op dat maximum.

Te vaak vragen: de Kiss-o'-Death

Bij het testen vroeg ik binnen een seconde meerdere keren de tijd aan ntp.time.nl. Het tweede antwoord was anders: leap indicator 3 ("klok niet gesynchroniseerd"), stratum 0 en als referentie-ID de bytes 82, 65, 84, 69. In ASCII is dat RATE.

Dat is een Kiss-o'-Death-pakket. Bij stratum 0 is de referentie-ID volgens RFC 5905 een "kiss code", en bij RATE moet de client meteen minder vaak vragen. Mijn script deed dat niet; de volgende vraag kreeg binnen 3 seconden geen antwoord.

Waarom certificaten falen bij een verkeerde klok

Het certificaat van wallieweb.nl geldt van 21 september 2026 07:09:51 UTC tot 20 december 2026 07:09:50 UTC. Of het nu geldig is, beslist je apparaat met zijn eigen klok. Met openssl verify -attime kun je een andere klok nabootsen:

klok op 15 okt 2026:               c1.pem: OK
klok op 21 sep 2026, 06:09 UTC:    error 9 at 0 depth lookup: certificate is not yet valid
klok op 20 dec 2026, 07:10 UTC:    error 10 at 0 depth lookup: certificate has expired

Een uur te vroeg is genoeg. Op 1 januari 2020 faalt zelfs ISRG Root X2, hoger in de keten. Hoe de browser daarover klaagt, staat in Hoe je browser een certificaat vertrouwt.

Hier zit een kip-en-eiprobleem. Gewone NTP-pakketten zijn standaard niet beveiligd, dus iemand op het pad kan je een verkeerde tijd sturen. Network Time Security (RFC 8915, september 2020) spreekt daarom eerst sleutels af via TLS op TCP-poort 4460; time.cloudflare.com deed die handshake bij mij netjes. Maar om dat TLS-certificaat te controleren, heb je de juiste tijd al nodig. De RFC erkent dat en raadt onder meer aan de tijd regelmatig op te slaan en geen certificaat te accepteren dat vóór die opgeslagen tijd verliep. systemd-timesyncd doet iets vergelijkbaars: bij het opstarten zet hij de klok minstens op de wijzigingstijd van het lege bestand /var/lib/systemd/timesync/clock.

Zelf proberen

Op Linux met systemd:

$ timedatectl timesync-status
       Server: 185.71.60.123 (1.debian.pool.ntp.org)
Poll interval: 34min 8s (min: 32s; max 34min 8s)
      Stratum: 2
       Offset: -2.520ms
        Delay: 15.255ms
    Frequency: +17.121ppm

Of stel zelf de vraag met dit Python-script (ntpvraag.py):

import socket, struct, time, sys
EPOCH = 2208988800  # seconden tussen 1900 en 1970
server = sys.argv[1] if len(sys.argv) > 1 else "ntp1.time.nl"
ip = socket.gethostbyname(server)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.settimeout(3)
vraag = bytes([0x23]) + bytes(47)          # LI 0, versie 4, mode 3 (client)
t1 = time.time(); s.sendto(vraag, (ip, 123))
data, _ = s.recvfrom(48); t4 = time.time()
def ts(b): sec, frac = struct.unpack("!II", b); return sec - EPOCH + frac / 2**32
t2, t3 = ts(data[32:40]), ts(data[40:48])
print("stratum", data[1], "refid", data[12:16])
print("offset  %.3f ms" % (((t2 - t1) + (t3 - t4)) / 2 * 1000))
print("delay   %.3f ms" % (((t4 - t1) - (t3 - t2)) * 1000))
$ python3 ntpvraag.py ntp1.time.nl
stratum 1 refid b'TMNL'
offset  -0.246 ms
delay   18.712 ms

Wacht een paar seconden tussen pogingen, anders krijg je zelf een RATE terug.

Tot slot

Wat me verraste, was hoe gevoelig de rekensom is voor één verkeerd geplaatste regel. Mijn DNS-fout maakte van een klok die op een milliseconde na goed liep er een die een derde seconde achter leek te lopen, en die uitkomst zag er volkomen geloofwaardig uit. NTP kan zo'n asymmetrie niet zien. Dat de offsets naar zeven servers allemaal binnen 5,1 ms bleven, is eigenlijk het enige teken dat het hier goed ging.