feat: add NTP; fix networking bugs/regressions

This commit is contained in:
2026-07-29 16:32:18 +01:00
parent a288dee7df
commit d99dab45e5
26 changed files with 809 additions and 47 deletions
+1
View File
@@ -211,6 +211,7 @@ namespace montauk::abi {
// Path metadata (size, timestamps, mode). (const char* path, FileStat* out) -> 0, -1 on error/unsupported.
static constexpr uint64_t SYS_STAT = 152;
static constexpr uint64_t SYS_SETUNIXTIME = 153;
// Tunable parameters (for SYS_SDR_SETPARAM / SYS_SDR_GETPARAM).
static constexpr int SDR_PARAM_FREQ = 0; // center frequency, Hz
+5
View File
@@ -176,6 +176,7 @@ extern "C" {
#define MTK_SYS_FBFLIP 150
#define MTK_SYS_GETEXECPATH 151
#define MTK_SYS_STAT 152
#define MTK_SYS_SETUNIXTIME 153
/* @SYSCALLS-END */
#define MTK_SOCK_TCP 1
@@ -596,6 +597,10 @@ static inline void mtk_gettime(mtk_datetime *out) {
_mtk_syscall1(MTK_SYS_GETTIME, (long)out);
}
static inline int mtk_set_unix_time(int64_t unix_seconds) {
return (int)_mtk_syscall1(MTK_SYS_SETUNIXTIME, (long)unix_seconds);
}
static inline void mtk_settz(int offset_minutes) {
_mtk_syscall1(MTK_SYS_SETTZ, (long)offset_minutes);
}
+194
View File
@@ -0,0 +1,194 @@
/*
* 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
+3
View File
@@ -367,6 +367,9 @@ namespace montauk {
// Timekeeping (wall-clock)
inline void gettime(montauk::abi::DateTime* out) { syscall1(montauk::abi::SYS_GETTIME, (uint64_t)out); }
inline int set_unix_time(int64_t unix_seconds) {
return (int)syscall1(montauk::abi::SYS_SETUNIXTIME, (uint64_t)unix_seconds);
}
// Timezone offset (total minutes from UTC)
inline void settz(int offset_minutes) { syscall1(montauk::abi::SYS_SETTZ, (uint64_t)(int64_t)offset_minutes); }