/* * ntp.h * Small NTPv4 client for MontaukOS programs (RFC 5905). * Copyright (c) 2026 Daniel Hammer */ #pragma once #include 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