195 lines
6.6 KiB
C++
195 lines
6.6 KiB
C++
/*
|
|
* ntp.h
|
|
* Small NTPv4 client for MontaukOS programs (RFC 5905).
|
|
* Copyright (c) 2026 Daniel Hammer
|
|
*/
|
|
|
|
#pragma once
|
|
|
|
#include <montauk/syscall.h>
|
|
|
|
namespace montauk::ntp {
|
|
|
|
static constexpr uint16_t SERVER_PORT = 123;
|
|
static constexpr uint32_t PACKET_SIZE = 48;
|
|
static constexpr uint64_t UNIX_EPOCH_DELTA = 2208988800ULL;
|
|
static constexpr uint32_t MIN_QUERY_INTERVAL_MS = 64000;
|
|
|
|
enum Result {
|
|
OK = 0,
|
|
INVALID_SERVER = -1,
|
|
SOCKET_ERROR = -2,
|
|
SEND_ERROR = -3,
|
|
TIMEOUT = -4,
|
|
INVALID_REPLY = -5,
|
|
CLOCK_ERROR = -6,
|
|
SERVER_REFUSED = -7,
|
|
};
|
|
|
|
using ProgressFn = void (*)(const char* message);
|
|
|
|
inline void progress(ProgressFn callback, const char* message) {
|
|
if (callback) callback(message);
|
|
}
|
|
|
|
inline const char* result_string(int result) {
|
|
switch (result) {
|
|
case OK: return "Time synchronized";
|
|
case INVALID_SERVER: return "Could not resolve NTP server";
|
|
case SOCKET_ERROR: return "Could not open NTP socket";
|
|
case SEND_ERROR: return "Could not send NTP request";
|
|
case TIMEOUT: return "NTP server timed out";
|
|
case INVALID_REPLY: return "NTP server returned an invalid reply";
|
|
case CLOCK_ERROR: return "Could not set the system clock";
|
|
case SERVER_REFUSED: return "NTP server refused frequent requests";
|
|
default: return "NTP synchronization failed";
|
|
}
|
|
}
|
|
|
|
inline uint32_t read_be32(const uint8_t* p) {
|
|
return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) |
|
|
((uint32_t)p[2] << 8) | (uint32_t)p[3];
|
|
}
|
|
|
|
inline void write_be64(uint8_t* p, uint64_t value) {
|
|
for (int i = 7; i >= 0; i--) {
|
|
p[i] = (uint8_t)value;
|
|
value >>= 8;
|
|
}
|
|
}
|
|
|
|
inline bool is_leap_year(int year) {
|
|
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
|
|
}
|
|
|
|
inline int days_in_month(int month, int year) {
|
|
static const int days[] = {0, 31, 28, 31, 30, 31, 30,
|
|
31, 31, 30, 31, 30, 31};
|
|
return month == 2 && is_leap_year(year) ? 29 : days[month];
|
|
}
|
|
|
|
inline int64_t current_unix_seconds() {
|
|
montauk::abi::DateTime now = {};
|
|
montauk::gettime(&now);
|
|
if (now.Year < 1970 || now.Month < 1 || now.Month > 12 ||
|
|
now.Day < 1 || now.Day > days_in_month(now.Month, now.Year))
|
|
return 0;
|
|
|
|
int64_t days = 0;
|
|
for (int year = 1970; year < (int)now.Year; year++)
|
|
days += is_leap_year(year) ? 366 : 365;
|
|
for (int month = 1; month < (int)now.Month; month++)
|
|
days += days_in_month(month, now.Year);
|
|
days += now.Day - 1;
|
|
|
|
int64_t local = days * 86400 + (int64_t)now.Hour * 3600 +
|
|
(int64_t)now.Minute * 60 + now.Second;
|
|
return local - (int64_t)montauk::gettz() * 60;
|
|
}
|
|
|
|
inline bool same_bytes(const uint8_t* a, const uint8_t* b, int count) {
|
|
for (int i = 0; i < count; i++) {
|
|
if (a[i] != b[i]) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
inline int synchronize(const char* server, uint32_t timeout_ms = 5000,
|
|
int64_t* out_unix_seconds = nullptr,
|
|
ProgressFn progress_callback = nullptr) {
|
|
if (!server || !server[0]) return INVALID_SERVER;
|
|
|
|
progress(progress_callback, "Opening NTP socket...");
|
|
int fd = montauk::socket(montauk::abi::SOCK_UDP);
|
|
if (fd < 0) return SOCKET_ERROR;
|
|
|
|
progress(progress_callback, "Resolving NTP server...");
|
|
uint32_t server_ip = montauk::resolve(server);
|
|
if (server_ip == 0) {
|
|
montauk::closesocket(fd);
|
|
return INVALID_SERVER;
|
|
}
|
|
|
|
uint8_t request[PACKET_SIZE] = {};
|
|
request[0] = 0x23; // leap=0, version=4, mode=3 (client)
|
|
request[2] = 6; // poll exponent: 2^6 = 64 seconds
|
|
request[3] = 0xEC; // precision: approximately 2^-20 seconds
|
|
|
|
int64_t unix_now = current_unix_seconds();
|
|
if (unix_now <= 0) {
|
|
montauk::closesocket(fd);
|
|
return CLOCK_ERROR;
|
|
}
|
|
uint64_t transmit_timestamp =
|
|
((uint64_t)unix_now + UNIX_EPOCH_DELTA) << 32;
|
|
write_be64(request + 40, transmit_timestamp);
|
|
|
|
progress(progress_callback, "Sending NTP request...");
|
|
if (montauk::sendto(fd, request, sizeof(request), server_ip, SERVER_PORT) < 0) {
|
|
montauk::closesocket(fd);
|
|
return SEND_ERROR;
|
|
}
|
|
|
|
progress(progress_callback, "Waiting for NTP reply...");
|
|
uint64_t deadline = montauk::get_milliseconds() + timeout_ms;
|
|
uint8_t reply[PACKET_SIZE];
|
|
for (;;) {
|
|
uint32_t source_ip = 0;
|
|
uint16_t source_port = 0;
|
|
int received = montauk::recvfrom(fd, reply, sizeof(reply), &source_ip, &source_port);
|
|
if (received >= (int)PACKET_SIZE && source_port == SERVER_PORT) {
|
|
progress(progress_callback, "Validating NTP reply...");
|
|
uint8_t leap = reply[0] >> 6;
|
|
uint8_t version = (reply[0] >> 3) & 7;
|
|
uint8_t mode = reply[0] & 7;
|
|
uint8_t stratum = reply[1];
|
|
if (leap == 3 || version < 3 || mode != 4 ||
|
|
!same_bytes(reply + 24, request + 40, 8)) {
|
|
montauk::closesocket(fd);
|
|
return INVALID_REPLY;
|
|
}
|
|
// Stratum zero is a Kiss-o'-Death response, commonly returned
|
|
// when a client sends requests too frequently.
|
|
if (stratum == 0) {
|
|
montauk::closesocket(fd);
|
|
return SERVER_REFUSED;
|
|
}
|
|
if (stratum > 15) {
|
|
montauk::closesocket(fd);
|
|
return INVALID_REPLY;
|
|
}
|
|
|
|
uint32_t ntp_seconds = read_be32(reply + 40);
|
|
uint32_t fraction = read_be32(reply + 44);
|
|
if ((uint64_t)ntp_seconds < UNIX_EPOCH_DELTA) {
|
|
montauk::closesocket(fd);
|
|
return INVALID_REPLY;
|
|
}
|
|
|
|
int64_t unix_seconds =
|
|
(int64_t)((uint64_t)ntp_seconds - UNIX_EPOCH_DELTA);
|
|
if (fraction >= 0x80000000u) unix_seconds++;
|
|
progress(progress_callback, "Applying synchronized time...");
|
|
int applied = montauk::set_unix_time(unix_seconds);
|
|
progress(progress_callback, "Closing NTP socket...");
|
|
montauk::closesocket(fd);
|
|
if (applied < 0) return CLOCK_ERROR;
|
|
if (out_unix_seconds) *out_unix_seconds = unix_seconds;
|
|
return OK;
|
|
}
|
|
|
|
uint64_t now = montauk::get_milliseconds();
|
|
if (now >= deadline) {
|
|
montauk::closesocket(fd);
|
|
return TIMEOUT;
|
|
}
|
|
// Poll with a short sleep instead of blocking on the socket handle.
|
|
// This keeps the timeout owned by this loop and avoids a missed socket
|
|
// wake leaving a caller blocked after its deadline.
|
|
uint64_t remaining = deadline - now;
|
|
montauk::sleep_ms(remaining < 10 ? remaining : 10);
|
|
}
|
|
}
|
|
|
|
} // namespace montauk::ntp
|