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 -1
View File
@@ -12,4 +12,4 @@
#pragma once #pragma once
#define MONTAUK_BUILD_NUMBER 8 #define MONTAUK_BUILD_NUMBER 17
+5
View File
@@ -12,6 +12,7 @@
#include <Timekeeping/ApicTimer.hpp> #include <Timekeeping/ApicTimer.hpp>
#include <Net/Icmp.hpp> #include <Net/Icmp.hpp>
#include <Net/Dns.hpp> #include <Net/Dns.hpp>
#include <Net/Ipv4.hpp>
#include <Net/Socket.hpp> #include <Net/Socket.hpp>
#include <Net/NetConfig.hpp> #include <Net/NetConfig.hpp>
#include <Drivers/Net/E1000.hpp> #include <Drivers/Net/E1000.hpp>
@@ -82,6 +83,10 @@ namespace montauk::abi {
static int Sys_RecvFrom(int fd, uint8_t* buf, uint32_t maxLen, static int Sys_RecvFrom(int fd, uint8_t* buf, uint32_t maxLen,
uint32_t* srcIp, uint16_t* srcPort) { uint32_t* srcIp, uint16_t* srcPort) {
// A preceding UDP send may be queued while ARP resolves the next hop.
// Drive the pending IPv4 queue on each non-blocking receive so a lost
// initial ARP request is retried without unrelated network traffic.
Net::Ipv4::FlushPending();
return Net::Socket::RecvFrom(fd, buf, maxLen, srcIp, srcPort, Sched::GetCurrentPid()); return Net::Socket::RecvFrom(fd, buf, maxLen, srcIp, srcPort, Sched::GetCurrentPid());
} }
+3 -1
View File
@@ -174,6 +174,8 @@ namespace montauk::abi {
if (!UserMemory::Writable<DateTime>(frame->arg1)) return -1; if (!UserMemory::Writable<DateTime>(frame->arg1)) return -1;
Sys_GetTime((DateTime*)frame->arg1); Sys_GetTime((DateTime*)frame->arg1);
return 0; return 0;
case SYS_SETUNIXTIME:
return Sys_SetUnixTime((int64_t)frame->arg1);
case SYS_SOCKET: case SYS_SOCKET:
return (int64_t)Sys_Socket((int)frame->arg1); return (int64_t)Sys_Socket((int)frame->arg1);
case SYS_CONNECT: case SYS_CONNECT:
@@ -556,7 +558,7 @@ namespace montauk::abi {
Hal::WriteMSR(Hal::IA32_FMASK, 0x200); Hal::WriteMSR(Hal::IA32_FMASK, 0x200);
Kt::KernelLogStream(Kt::OK, "Syscall") << "SYSCALL/SYSRET initialized (LSTAR=" Kt::KernelLogStream(Kt::OK, "Syscall") << "SYSCALL/SYSRET initialized (LSTAR="
<< kcp::hex << (uint64_t)SyscallEntry << kcp::dec << ", 124 syscall slots)"; << kcp::hex << (uint64_t)SyscallEntry << kcp::dec << ", 154 syscall slots)";
} }
} }
+1
View File
@@ -287,6 +287,7 @@ namespace montauk::abi {
// Path metadata (size, timestamps, mode). (const char* path, FileStat* out) -> 0, -1 on error/unsupported. // 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_STAT = 152;
static constexpr uint64_t SYS_SETUNIXTIME = 153;
// Tunable parameters (for SYS_SDR_SETPARAM / SYS_SDR_GETPARAM). // Tunable parameters (for SYS_SDR_SETPARAM / SYS_SDR_GETPARAM).
static constexpr int SDR_PARAM_FREQ = 0; // center frequency, Hz static constexpr int SDR_PARAM_FREQ = 0; // center frequency, Hz
+5 -1
View File
@@ -38,4 +38,8 @@ namespace montauk::abi {
static int64_t Sys_GetTZ() { static int64_t Sys_GetTZ() {
return (int64_t)Timekeeping::GetTZOffset(); return (int64_t)Timekeeping::GetTZOffset();
} }
};
static int64_t Sys_SetUnixTime(int64_t unixSeconds) {
return Timekeeping::SetUnixTimestamp(unixSeconds) ? 0 : -1;
}
};
+4 -1
View File
@@ -250,7 +250,10 @@ namespace Drivers::Net::E1000 {
KernelLogStream(INFO, "E1000") << "Link status change: " << (linkUp ? "UP" : "DOWN"); KernelLogStream(INFO, "E1000") << "Link status change: " << (linkUp ? "UP" : "DOWN");
} }
if (icr & ICR_RXT0) { // Both receive-timer and descriptor-threshold causes mean completed
// RX descriptors may be waiting. ICR is clear-on-read, so ignoring
// RXDMT0 can strand a lone packet until unrelated traffic arrives.
if (icr & (ICR_RXT0 | ICR_RXDMT0)) {
// Process received packets // Process received packets
while (true) { while (true) {
uint32_t nextIdx = (g_rxTail + 1) % RX_DESC_COUNT; uint32_t nextIdx = (g_rxTail + 1) % RX_DESC_COUNT;
+4 -1
View File
@@ -477,7 +477,10 @@ namespace Drivers::Net::E1000E {
KernelLogStream(INFO, "E1000E") << "Link status change: " << (linkUp ? "UP" : "DOWN"); KernelLogStream(INFO, "E1000E") << "Link status change: " << (linkUp ? "UP" : "DOWN");
} }
if (icr & ICR_RXT0) { // Both receive-timer and descriptor-threshold causes mean completed
// RX descriptors may be waiting. ICR is clear-on-read, so ignoring
// RXDMT0 can strand a lone packet until unrelated traffic arrives.
if (icr & (ICR_RXT0 | ICR_RXDMT0)) {
while (true) { while (true) {
uint32_t nextIdx = (g_rxTail + 1) % RX_DESC_COUNT; uint32_t nextIdx = (g_rxTail + 1) % RX_DESC_COUNT;
RxDescriptor& desc = g_rxDescs[nextIdx]; RxDescriptor& desc = g_rxDescs[nextIdx];
+11 -1
View File
@@ -8,6 +8,7 @@
#include <Net/Udp.hpp> #include <Net/Udp.hpp>
#include <Net/ByteOrder.hpp> #include <Net/ByteOrder.hpp>
#include <Net/NetConfig.hpp> #include <Net/NetConfig.hpp>
#include <Net/Ipv4.hpp>
#include <Libraries/Memory.hpp> #include <Libraries/Memory.hpp>
#include <Libraries/String.hpp> #include <Libraries/String.hpp>
#include <Timekeeping/ApicTimer.hpp> #include <Timekeeping/ApicTimer.hpp>
@@ -409,12 +410,21 @@ namespace Net::Dns {
// Wait for response with timeout // Wait for response with timeout
uint64_t start = Timekeeping::GetMilliseconds(); uint64_t start = Timekeeping::GetMilliseconds();
uint64_t lastPendingService = start;
while (!query->gotResponse) { while (!query->gotResponse) {
if (Timekeeping::GetMilliseconds() - start >= timeoutMs) { uint64_t now = Timekeeping::GetMilliseconds();
if (now - start >= timeoutMs) {
Net::Udp::Unbind(query->localPort); Net::Udp::Unbind(query->localPort);
query->active = false; query->active = false;
return 0; return 0;
} }
// Ipv4::Send may have queued this DNS datagram while resolving the
// gateway MAC. Drive that pending queue here so ARP coalescing can
// retry a lost initial request without requiring unrelated traffic.
if (now - lastPendingService >= 100) {
Net::Ipv4::FlushPending();
lastPendingService = now;
}
Sched::Schedule(); Sched::Schedule();
} }
+8 -1
View File
@@ -84,6 +84,13 @@ int64_t Timekeeping::GetUnixTimestamp() {
return g_bootEpoch + (int64_t)(Timekeeping::GetMilliseconds() / 1000); return g_bootEpoch + (int64_t)(Timekeeping::GetMilliseconds() / 1000);
} }
bool Timekeeping::SetUnixTimestamp(int64_t unixSeconds) {
if (unixSeconds < 0 || unixSeconds > 4102444799LL)
return false;
g_bootEpoch = unixSeconds - (int64_t)(Timekeeping::GetMilliseconds() / 1000);
return true;
}
Timekeeping::DateTime Timekeeping::GetDateTime() { Timekeeping::DateTime Timekeeping::GetDateTime() {
return EpochToDate(GetUnixTimestamp() + (int64_t)g_tzOffsetMinutes * 60); return EpochToDate(GetUnixTimestamp() + (int64_t)g_tzOffsetMinutes * 60);
} }
@@ -94,4 +101,4 @@ void Timekeeping::SetTZOffset(int totalMinutes) {
int Timekeeping::GetTZOffset() { int Timekeeping::GetTZOffset() {
return g_tzOffsetMinutes; return g_tzOffsetMinutes;
} }
+1
View File
@@ -53,6 +53,7 @@ namespace Timekeeping {
void Init(uint16_t Year, uint8_t Month, uint8_t Day, uint8_t Hour, uint8_t Minute, uint8_t Second); void Init(uint16_t Year, uint8_t Month, uint8_t Day, uint8_t Hour, uint8_t Minute, uint8_t Second);
int64_t GetUnixTimestamp(); int64_t GetUnixTimestamp();
DateTime GetDateTime(); DateTime GetDateTime();
bool SetUnixTimestamp(int64_t unixSeconds);
void SetTZOffset(int totalMinutes); void SetTZOffset(int totalMinutes);
int GetTZOffset(); int GetTZOffset();
@@ -133,7 +133,7 @@
<h3>Time zone selection</h3> <h3>Time zone selection</h3>
<ol> <ol>
<li>Click on the application menu icon located at the top-left corner of the desktop, and click the 'Settings' entry.</li> <li>Click on the application menu icon located at the top-left corner of the desktop, and click the 'Settings' entry.</li>
<li>Double-click the 'Time Zone' icon within the virtual 'Settings' folder.</li> <li>Double-click the 'Time' icon within the virtual 'Settings' folder, then open the Time Zones tab.</li>
<li>The default time zone is Oslo, Norway. To adjust your time zone, scroll along the left pane (countries) and select your country or region. Then, select the city closest to your location on the right pane.</li> <li>The default time zone is Oslo, Norway. To adjust your time zone, scroll along the left pane (countries) and select your country or region. Then, select the city closest to your location on the right pane.</li>
<li>Click 'Apply'.</li> <li>Click 'Apply'.</li>
</ol> </ol>
+1 -1
View File
@@ -220,7 +220,7 @@ charmap: libc
printers: bearssl libc tls printers: bearssl libc tls
$(MAKE) -C src/printers $(MAKE) -C src/printers
# Build time zone standalone GUI tool (depends on libc). # Build Time standalone GUI tool (depends on libc).
timezone: libc timezone: libc
$(MAKE) -C src/timezone $(MAKE) -C src/timezone
+3
View File
@@ -0,0 +1,3 @@
[ntp]
enabled = true
server = "pool.ntp.org"
+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. // 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_STAT = 152;
static constexpr uint64_t SYS_SETUNIXTIME = 153;
// Tunable parameters (for SYS_SDR_SETPARAM / SYS_SDR_GETPARAM). // Tunable parameters (for SYS_SDR_SETPARAM / SYS_SDR_GETPARAM).
static constexpr int SDR_PARAM_FREQ = 0; // center frequency, Hz 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_FBFLIP 150
#define MTK_SYS_GETEXECPATH 151 #define MTK_SYS_GETEXECPATH 151
#define MTK_SYS_STAT 152 #define MTK_SYS_STAT 152
#define MTK_SYS_SETUNIXTIME 153
/* @SYSCALLS-END */ /* @SYSCALLS-END */
#define MTK_SOCK_TCP 1 #define MTK_SOCK_TCP 1
@@ -596,6 +597,10 @@ static inline void mtk_gettime(mtk_datetime *out) {
_mtk_syscall1(MTK_SYS_GETTIME, (long)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) { static inline void mtk_settz(int offset_minutes) {
_mtk_syscall1(MTK_SYS_SETTZ, (long)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) // Timekeeping (wall-clock)
inline void gettime(montauk::abi::DateTime* out) { syscall1(montauk::abi::SYS_GETTIME, (uint64_t)out); } 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) // Timezone offset (total minutes from UTC)
inline void settz(int offset_minutes) { syscall1(montauk::abi::SYS_SETTZ, (uint64_t)(int64_t)offset_minutes); } inline void settz(int offset_minutes) { syscall1(montauk::abi::SYS_SETTZ, (uint64_t)(int64_t)offset_minutes); }
+1
View File
@@ -163,6 +163,7 @@ extern "C" void _start() {
// ---- Stage 1: Network configuration (non-blocking) ---- // ---- Stage 1: Network configuration (non-blocking) ----
run_service("0:/os/dhcp.elf", "dhcp", false); run_service("0:/os/dhcp.elf", "dhcp", false);
run_service("0:/os/ntp.elf", "network time", false);
// Printing is an optional install component; skip quietly when absent. // Printing is an optional install component; skip quietly when absent.
if (service_installed("0:/os/printd.elf")) if (service_installed("0:/os/printd.elf"))
+81
View File
@@ -0,0 +1,81 @@
/*
* main.cpp
* MontaukOS Network Time Protocol service
* Copyright (c) 2026 Daniel Hammer
*/
#include <montauk/config.h>
#include <montauk/ntp.h>
#include <montauk/string.h>
#include <montauk/syscall.h>
static constexpr uint64_t RETRY_INTERVAL_MS =
montauk::ntp::MIN_QUERY_INTERVAL_MS;
static constexpr uint64_t SYNC_INTERVAL_MS = 60ULL * 60ULL * 1000ULL;
static constexpr uint64_t CONFIG_POLL_MS = 5000;
static void log(const char* message) {
montauk::print("ntp: ");
montauk::print(message);
montauk::print("\n");
}
static bool load_settings(char* server, int server_cap) {
auto cfg = montauk::config::load("ntp");
bool enabled = cfg.get_bool("ntp.enabled", true);
montauk::strncpy(server, cfg.get_string("ntp.server", "pool.ntp.org"),
server_cap);
cfg.destroy();
if (!server[0])
montauk::strncpy(server, "pool.ntp.org", server_cap);
return enabled;
}
extern "C" void _start() {
log("service started");
char active_server[128] = {};
bool was_enabled = false;
uint64_t next_attempt = 0;
for (;;) {
char server[128];
bool enabled = load_settings(server, sizeof(server));
bool server_changed = !montauk::streq(server, active_server);
if (server_changed) {
montauk::strncpy(active_server, server, sizeof(active_server));
next_attempt = 0;
}
if (!enabled) {
was_enabled = false;
next_attempt = 0;
montauk::sleep_ms(CONFIG_POLL_MS);
continue;
}
if (!was_enabled) {
was_enabled = true;
next_attempt = 0;
}
uint64_t now = montauk::get_milliseconds();
if (next_attempt != 0 && now < next_attempt) {
montauk::sleep_ms(CONFIG_POLL_MS);
continue;
}
montauk::abi::NetCfg net = {};
montauk::get_netcfg(&net);
if (net.ipAddress == 0 || net.dnsServer == 0) {
next_attempt = now + RETRY_INTERVAL_MS;
montauk::sleep_ms(CONFIG_POLL_MS);
continue;
}
int result = montauk::ntp::synchronize(server);
log(montauk::ntp::result_string(result));
next_attempt = montauk::get_milliseconds() +
(result == montauk::ntp::OK ? SYNC_INTERVAL_MS : RETRY_INTERVAL_MS);
montauk::sleep_ms(CONFIG_POLL_MS);
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
# Makefile for timezone (standalone Time Zone configuration app) on MontaukOS # Makefile for timezone (standalone Time configuration app) on MontaukOS
# Copyright (c) 2026 Daniel Hammer # Copyright (c) 2026 Daniel Hammer
MAKEFLAGS += -rR MAKEFLAGS += -rR
+271 -36
View File
@@ -1,6 +1,6 @@
/* /*
* main.cpp * main.cpp
* MontaukOS Time Zone configuration app * MontaukOS Time configuration app
* Copyright (c) 2026 Daniel Hammer * Copyright (c) 2026 Daniel Hammer
*/ */
@@ -8,6 +8,7 @@
#include <montauk/heap.h> #include <montauk/heap.h>
#include <montauk/string.h> #include <montauk/string.h>
#include <montauk/syscall.h> #include <montauk/syscall.h>
#include <montauk/ntp.h>
#include <gui/gui.hpp> #include <gui/gui.hpp>
#include <gui/canvas.hpp> #include <gui/canvas.hpp>
#include <gui/mtk.hpp> #include <gui/mtk.hpp>
@@ -23,13 +24,25 @@ using namespace gui;
static constexpr int INIT_W = 640; static constexpr int INIT_W = 640;
static constexpr int INIT_H = 460; static constexpr int INIT_H = 460;
static constexpr int HEADER_H = 14; static constexpr int TAB_H = 36;
static constexpr int FOOTER_H = 58; static constexpr int FOOTER_H = 58;
static constexpr int PAD = 16; static constexpr int PAD = 16;
static constexpr int GAP = 12; static constexpr int GAP = 12;
static constexpr int LABEL_H = 24; static constexpr int LABEL_H = 24;
static constexpr int ROW_H = 30; static constexpr int ROW_H = 30;
static constexpr int MAX_COUNTRIES = 320; static constexpr int MAX_COUNTRIES = 320;
static constexpr int NTP_SERVER_CAP = 128;
enum Tab {
TAB_TIME_ZONES = 0,
TAB_NTP = 1,
TAB_COUNT = 2,
};
static const char* const kTabLabels[TAB_COUNT] = {
"Time Zones",
"NTP",
};
struct CountryEntry { struct CountryEntry {
char key[64]; // Full TOML table key, e.g. countries.NO char key[64]; // Full TOML table key, e.g. countries.NO
@@ -43,6 +56,7 @@ struct CountryEntry {
}; };
static WsWindow g_win; static WsWindow g_win;
static Tab g_tab = TAB_TIME_ZONES;
static montauk::toml::Doc g_data; static montauk::toml::Doc g_data;
static bool g_data_loaded = false; static bool g_data_loaded = false;
static CountryEntry g_countries[MAX_COUNTRIES]; static CountryEntry g_countries[MAX_COUNTRIES];
@@ -60,10 +74,20 @@ static int g_focus_pane = 0; // 0=country list, 1=zone list
static int g_mouse_x = -1; static int g_mouse_x = -1;
static int g_mouse_y = -1; static int g_mouse_y = -1;
static bool g_dirty = false; static bool g_dirty = false;
static char g_ntp_server[NTP_SERVER_CAP] = "pool.ntp.org";
static char g_saved_ntp_server[NTP_SERVER_CAP] = "pool.ntp.org";
static mtk::TextInputState g_ntp_input = {};
static bool g_ntp_enabled = true;
static bool g_saved_ntp_enabled = true;
static bool g_ntp_dirty = false;
static bool g_ntp_syncing = false;
static uint64_t g_last_clock_render = 0;
static char g_status[160] = {}; static char g_status[160] = {};
static uint64_t g_status_time = 0; static uint64_t g_status_time = 0;
static Color g_accent = colors::ACCENT; static Color g_accent = colors::ACCENT;
static void render();
static void safe_copy(char* dst, int cap, const char* src) { static void safe_copy(char* dst, int cap, const char* src) {
montauk::strncpy(dst, src ? src : "", cap); montauk::strncpy(dst, src ? src : "", cap);
} }
@@ -217,8 +241,8 @@ static void clamp_scrolls() {
Rect country_list = {}; Rect country_list = {};
Rect zone_list = {}; Rect zone_list = {};
int left_w = gui_min(250, (g_win.width - PAD * 2 - GAP) / 2); int left_w = gui_min(250, (g_win.width - PAD * 2 - GAP) / 2);
int list_y = HEADER_H + LABEL_H; int list_y = TAB_H + LABEL_H;
int list_h = gui_max(g_win.height - HEADER_H - FOOTER_H - LABEL_H - PAD, ROW_H); int list_h = gui_max(g_win.height - TAB_H - FOOTER_H - LABEL_H - PAD, ROW_H);
country_list = {PAD, list_y, left_w, list_h}; country_list = {PAD, list_y, left_w, list_h};
zone_list = {PAD + left_w + GAP, list_y, zone_list = {PAD + left_w + GAP, list_y,
g_win.width - PAD * 2 - GAP - left_w, list_h}; g_win.width - PAD * 2 - GAP - left_w, list_h};
@@ -235,8 +259,8 @@ static void clamp_scrolls() {
} }
static void ensure_country_visible() { static void ensure_country_visible() {
Rect list = {PAD, HEADER_H + LABEL_H, 250, Rect list = {PAD, TAB_H + LABEL_H, 250,
gui_max(g_win.height - HEADER_H - FOOTER_H - LABEL_H - PAD, ROW_H)}; gui_max(g_win.height - TAB_H - FOOTER_H - LABEL_H - PAD, ROW_H)};
int rows = visible_rows(list); int rows = visible_rows(list);
if (g_selected_country < g_country_scroll) if (g_selected_country < g_country_scroll)
g_country_scroll = g_selected_country; g_country_scroll = g_selected_country;
@@ -246,9 +270,9 @@ static void ensure_country_visible() {
static void ensure_zone_visible() { static void ensure_zone_visible() {
int left_w = gui_min(250, (g_win.width - PAD * 2 - GAP) / 2); int left_w = gui_min(250, (g_win.width - PAD * 2 - GAP) / 2);
Rect list = {PAD + left_w + GAP, HEADER_H + LABEL_H, Rect list = {PAD + left_w + GAP, TAB_H + LABEL_H,
g_win.width - PAD * 2 - GAP - left_w, g_win.width - PAD * 2 - GAP - left_w,
gui_max(g_win.height - HEADER_H - FOOTER_H - LABEL_H - PAD, ROW_H)}; gui_max(g_win.height - TAB_H - FOOTER_H - LABEL_H - PAD, ROW_H)};
int rows = visible_rows(list); int rows = visible_rows(list);
if (g_selected_zone < g_zone_scroll) if (g_selected_zone < g_zone_scroll)
g_zone_scroll = g_selected_zone; g_zone_scroll = g_selected_zone;
@@ -357,6 +381,73 @@ static void load_saved_selection() {
clamp_scrolls(); clamp_scrolls();
} }
static void load_ntp_settings() {
auto cfg = montauk::config::load("ntp");
g_ntp_enabled = cfg.get_bool("ntp.enabled", true);
safe_copy(g_ntp_server, sizeof(g_ntp_server),
cfg.get_string("ntp.server", "pool.ntp.org"));
cfg.destroy();
if (!g_ntp_server[0])
safe_copy(g_ntp_server, sizeof(g_ntp_server), "pool.ntp.org");
safe_copy(g_saved_ntp_server, sizeof(g_saved_ntp_server), g_ntp_server);
g_saved_ntp_enabled = g_ntp_enabled;
g_ntp_dirty = false;
mtk::text_input_reset(g_ntp_input, montauk::slen(g_ntp_server));
}
static bool ntp_settings_changed() {
return g_ntp_enabled != g_saved_ntp_enabled ||
!montauk::streq(g_ntp_server, g_saved_ntp_server);
}
static void save_ntp_settings() {
if (!g_ntp_server[0]) {
set_status("Enter an NTP server");
return;
}
auto cfg = montauk::config::load("ntp");
montauk::config::set_bool(&cfg, "ntp.enabled", g_ntp_enabled);
montauk::config::set_string(&cfg, "ntp.server", g_ntp_server);
int result = montauk::config::save("ntp", &cfg);
cfg.destroy();
if (result < 0) {
set_status("Could not save NTP settings");
return;
}
safe_copy(g_saved_ntp_server, sizeof(g_saved_ntp_server), g_ntp_server);
g_saved_ntp_enabled = g_ntp_enabled;
g_ntp_dirty = false;
set_status("NTP settings saved");
}
static void revert_ntp_settings() {
safe_copy(g_ntp_server, sizeof(g_ntp_server), g_saved_ntp_server);
g_ntp_enabled = g_saved_ntp_enabled;
g_ntp_dirty = false;
mtk::text_input_reset(g_ntp_input, montauk::slen(g_ntp_server));
set_status("NTP changes reverted");
}
static void ntp_progress(const char* message) {
set_status(message);
render();
}
static void synchronize_ntp() {
if (g_ntp_syncing) return;
if (!g_ntp_server[0]) {
set_status("Enter an NTP server");
return;
}
g_ntp_syncing = true;
int result = montauk::ntp::synchronize(g_ntp_server, 5000, nullptr,
ntp_progress);
g_ntp_syncing = false;
set_status(montauk::ntp::result_string(result));
}
static void fit_text(char* out, int cap, const char* text, int max_w) { static void fit_text(char* out, int cap, const char* text, int max_w) {
safe_copy(out, cap, text); safe_copy(out, cap, text);
if (text_width(out) <= max_w) return; if (text_width(out) <= max_w) return;
@@ -482,8 +573,8 @@ static void revert_selection() {
static Rect country_list_rect() { static Rect country_list_rect() {
int left_w = gui_min(250, (g_win.width - PAD * 2 - GAP) / 2); int left_w = gui_min(250, (g_win.width - PAD * 2 - GAP) / 2);
return {PAD, HEADER_H + LABEL_H, left_w, return {PAD, TAB_H + LABEL_H, left_w,
gui_max(g_win.height - HEADER_H - FOOTER_H - LABEL_H - PAD, ROW_H)}; gui_max(g_win.height - TAB_H - FOOTER_H - LABEL_H - PAD, ROW_H)};
} }
static Rect zone_list_rect() { static Rect zone_list_rect() {
@@ -500,6 +591,30 @@ static Rect revert_button_rect() {
return {g_win.width - PAD - 104 - GAP - 92, g_win.height - FOOTER_H + 14, 92, 30}; return {g_win.width - PAD - 104 - GAP - 92, g_win.height - FOOTER_H + 14, 92, 30};
} }
static Rect tab_bar_rect() {
return {0, 0, g_win.width, TAB_H};
}
static int ntp_server_field_y() {
return TAB_H + 24;
}
static Rect ntp_server_input_rect() {
return mtk::labeled_text_input_rect(PAD, ntp_server_field_y(),
g_win.width - PAD * 2,
app_theme(), 34);
}
static Rect ntp_enabled_rect() {
Rect input = ntp_server_input_rect();
return {PAD, input.y + input.h + 18, g_win.width - PAD * 2, 28};
}
static Rect ntp_sync_button_rect() {
Rect revert = revert_button_rect();
return {revert.x - GAP - 112, revert.y, 112, revert.h};
}
static bool mouse_in_rect(const Rect& rect) { static bool mouse_in_rect(const Rect& rect) {
return rect.contains(g_mouse_x, g_mouse_y); return rect.contains(g_mouse_x, g_mouse_y);
} }
@@ -644,50 +759,92 @@ static void draw_details(Canvas& canvas, const mtk::Theme& theme) {
} }
} }
static void draw_ntp_tab(Canvas& canvas, const mtk::Theme& theme) {
int field_y = ntp_server_field_y();
mtk::draw_labeled_text_field(canvas, PAD, field_y, g_win.width - PAD * 2,
"NTP Server", g_ntp_server,
g_ntp_input.cursor, true, false, theme, 34,
g_ntp_input.selection_anchor);
Rect enabled = ntp_enabled_rect();
mtk::draw_checkbox(canvas, enabled, "Synchronize time automatically",
mtk::check_state(g_ntp_enabled), theme, true,
enabled.contains(g_mouse_x, g_mouse_y));
canvas.text(PAD, enabled.y + enabled.h + 10,
"MontaukOS will synchronize at startup and periodically while enabled.",
theme.text_subtle);
montauk::abi::DateTime now = {};
montauk::gettime(&now);
char current[96];
snprintf(current, sizeof(current), "Current system time: %04u-%02u-%02u %02u:%02u:%02u",
(unsigned)now.Year, (unsigned)now.Month, (unsigned)now.Day,
(unsigned)now.Hour, (unsigned)now.Minute, (unsigned)now.Second);
int explanation_y = enabled.y + enabled.h + 10;
canvas.text(PAD, explanation_y + system_font_height() + 24,
current, theme.text);
}
static void render() { static void render() {
mtk::StandaloneHost host(&g_win); mtk::StandaloneHost host(&g_win);
Canvas canvas = host.canvas(); Canvas canvas = host.canvas();
mtk::Theme theme = app_theme(); mtk::Theme theme = app_theme();
canvas.fill(theme.window_bg); canvas.fill(theme.window_bg);
mtk::draw_tab_bar(canvas, tab_bar_rect(), kTabLabels, TAB_COUNT,
(int)g_tab, theme);
if (g_country_count <= 0) { if (g_tab == TAB_TIME_ZONES) {
canvas.text(PAD, HEADER_H + 24, if (g_country_count <= 0) {
"No time zone data was found in 0:/config/timezonedata.toml", canvas.text(PAD, TAB_H + 24,
theme.danger); "No time zone data was found in 0:/config/timezonedata.toml",
host.present(); theme.danger);
return; } else {
Rect country = country_list_rect();
Rect zone = zone_list_rect();
canvas.text(country.x, TAB_H + 6, "Country", theme.text_subtle);
canvas.text(zone.x, TAB_H + 6, "Time Zone", theme.text_subtle);
draw_country_list(canvas, country, theme);
draw_zone_list(canvas, zone, theme);
draw_details(canvas, theme);
}
} else {
draw_ntp_tab(canvas, theme);
} }
Rect country = country_list_rect();
Rect zone = zone_list_rect();
canvas.text(country.x, HEADER_H + 6, "Country", theme.text_subtle);
canvas.text(zone.x, HEADER_H + 6, "Time Zone", theme.text_subtle);
draw_country_list(canvas, country, theme);
draw_zone_list(canvas, zone, theme);
draw_details(canvas, theme);
Rect footer = {0, g_win.height - FOOTER_H, g_win.width, FOOTER_H}; Rect footer = {0, g_win.height - FOOTER_H, g_win.width, FOOTER_H};
canvas.fill_rect(footer.x, footer.y, footer.w, footer.h, theme.surface); canvas.fill_rect(footer.x, footer.y, footer.w, footer.h, theme.surface);
mtk::draw_separator(canvas, 0, footer.y, g_win.width, theme); mtk::draw_separator(canvas, 0, footer.y, g_win.width, theme);
const char* footer_text = status_visible() bool dirty = g_tab == TAB_TIME_ZONES ? g_dirty : g_ntp_dirty;
? g_status const char* footer_text = status_visible() ? g_status :
: (g_dirty ? "Unsaved time zone selection" : "Selection saved"); (g_tab == TAB_TIME_ZONES
int max_footer_text = revert_button_rect().x - PAD - GAP; ? (g_dirty ? "Unsaved time zone selection" : "Selection saved")
: (g_ntp_dirty ? "Unsaved NTP changes" : "NTP settings ready"));
int max_footer_text = (g_tab == TAB_NTP ? ntp_sync_button_rect().x
: revert_button_rect().x) - PAD - GAP;
char footer_label[160]; char footer_label[160];
fit_text(footer_label, sizeof(footer_label), footer_text, max_footer_text); fit_text(footer_label, sizeof(footer_label), footer_text, max_footer_text);
canvas.text(PAD, footer.y + (FOOTER_H - system_font_height()) / 2, canvas.text(PAD, footer.y + (FOOTER_H - system_font_height()) / 2,
footer_label, g_dirty ? theme.text : theme.text_subtle); footer_label, dirty ? theme.text : theme.text_subtle);
Rect revert = revert_button_rect(); Rect revert = revert_button_rect();
Rect apply = apply_button_rect(); Rect apply = apply_button_rect();
mtk::draw_button(canvas, revert, "Revert", mtk::BUTTON_SECONDARY, mtk::draw_button(canvas, revert, "Revert", mtk::BUTTON_SECONDARY,
button_state(revert, g_dirty), theme); button_state(revert, dirty), theme);
mtk::draw_button(canvas, apply, "Apply", mtk::BUTTON_PRIMARY, mtk::draw_button(canvas, apply, "Apply", mtk::BUTTON_PRIMARY,
button_state(apply, g_dirty), theme); button_state(apply, dirty), theme);
if (g_tab == TAB_NTP) {
Rect sync = ntp_sync_button_rect();
mtk::draw_button(canvas, sync, g_ntp_syncing ? "Syncing..." : "Sync Now",
mtk::BUTTON_PRIMARY,
button_state(sync, !g_ntp_syncing), theme);
mtk::draw_text_input_context_menu(
canvas, g_ntp_input, theme,
mtk::text_input_has_selection(g_ntp_input, g_ntp_server,
(int)sizeof(g_ntp_server)));
}
host.present(); host.present();
} }
@@ -698,6 +855,55 @@ static bool handle_mouse(const montauk::abi::WinEvent& ev) {
g_mouse_x = ev.mouse.x; g_mouse_x = ev.mouse.x;
g_mouse_y = ev.mouse.y; g_mouse_y = ev.mouse.y;
bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
bool right_clicked = (ev.mouse.buttons & 2) && !(ev.mouse.prev_buttons & 2);
if (clicked) {
int tab = mtk::hit_tab_bar(tab_bar_rect(), TAB_COUNT,
g_mouse_x, g_mouse_y);
if (tab >= 0) {
g_tab = (Tab)tab;
mtk::context_menu_close(g_ntp_input.context);
return true;
}
}
if (g_tab == TAB_NTP) {
Rect input = ntp_server_input_rect();
if (g_ntp_input.context.open || g_ntp_input.dragging ||
input.contains(g_mouse_x, g_mouse_y)) {
int result = mtk::text_input_handle_mouse(
g_ntp_input, input, g_ntp_server, (int)sizeof(g_ntp_server),
g_mouse_x, g_mouse_y, ev.mouse.buttons, ev.mouse.prev_buttons,
g_win.width, g_win.height, true);
if (result & mtk::TEXT_INPUT_CHANGED) {
g_ntp_dirty = ntp_settings_changed();
}
if (result != mtk::TEXT_INPUT_NONE) return true;
}
if (!clicked && !right_clicked)
return true;
if (clicked && ntp_enabled_rect().contains(g_mouse_x, g_mouse_y)) {
g_ntp_enabled = !g_ntp_enabled;
g_ntp_dirty = ntp_settings_changed();
return true;
}
if (clicked && !g_ntp_syncing &&
ntp_sync_button_rect().contains(g_mouse_x, g_mouse_y)) {
synchronize_ntp();
return true;
}
if (clicked && revert_button_rect().contains(g_mouse_x, g_mouse_y)) {
if (g_ntp_dirty) revert_ntp_settings();
return true;
}
if (clicked && apply_button_rect().contains(g_mouse_x, g_mouse_y)) {
if (g_ntp_dirty) save_ntp_settings();
return true;
}
return true;
}
Rect country = country_list_rect(); Rect country = country_list_rect();
Rect zone = zone_list_rect(); Rect zone = zone_list_rect();
Rect revert = revert_button_rect(); Rect revert = revert_button_rect();
@@ -745,7 +951,6 @@ static bool handle_mouse(const montauk::abi::WinEvent& ev) {
} }
} }
bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
if (!clicked) return redraw; if (!clicked) return redraw;
if (!csb_track.empty() && csb_track.contains(g_mouse_x, g_mouse_y)) { if (!csb_track.empty() && csb_track.contains(g_mouse_x, g_mouse_y)) {
@@ -804,7 +1009,31 @@ static bool handle_mouse(const montauk::abi::WinEvent& ev) {
} }
static bool handle_key(const montauk::abi::KeyEvent& key) { static bool handle_key(const montauk::abi::KeyEvent& key) {
if (!key.pressed || g_country_count <= 0) return false; if (!key.pressed) return false;
if (key.ascii == '\t') {
g_tab = g_tab == TAB_TIME_ZONES ? TAB_NTP : TAB_TIME_ZONES;
mtk::context_menu_close(g_ntp_input.context);
return true;
}
if (g_tab == TAB_NTP) {
if (key.ascii == '\n' || key.ascii == '\r') {
synchronize_ntp();
return true;
}
if (key.ascii == '\033') {
if (g_ntp_dirty) revert_ntp_settings();
return true;
}
int result = mtk::text_input_key(g_ntp_input, g_ntp_server,
(int)sizeof(g_ntp_server), key);
if (result & mtk::TEXT_INPUT_CHANGED)
g_ntp_dirty = ntp_settings_changed();
return (result & mtk::TEXT_INPUT_CONSUMED) != 0;
}
if (g_country_count <= 0) return false;
if (key.ascii == '\n' || key.ascii == '\r') { if (key.ascii == '\n' || key.ascii == '\r') {
if (g_dirty) save_selection(); if (g_dirty) save_selection();
@@ -851,10 +1080,11 @@ extern "C" void _start() {
load_accent(); load_accent();
load_timezone_data(); load_timezone_data();
if (!g_win.create("Time Zone", INIT_W, INIT_H)) if (!g_win.create("Time", INIT_W, INIT_H))
montauk::exit(1); montauk::exit(1);
load_saved_selection(); load_saved_selection();
load_ntp_settings();
render(); render();
while (g_win.id >= 0 && !g_win.closed) { while (g_win.id >= 0 && !g_win.closed) {
@@ -864,6 +1094,11 @@ extern "C" void _start() {
if (r < 0) break; if (r < 0) break;
if (r == 0) { if (r == 0) {
uint64_t now = montauk::get_milliseconds();
if (g_tab == TAB_NTP && now - g_last_clock_render >= 1000) {
g_last_clock_render = now;
render();
}
montauk::sleep_ms(16); montauk::sleep_ms(16);
continue; continue;
} }
+1 -1
View File
@@ -1,6 +1,6 @@
[app] [app]
id = "timezone" id = "timezone"
name = "Time Zone" name = "Time"
binary = "timezone.elf" binary = "timezone.elf"
icon = "preferences-system-time.svg" icon = "preferences-system-time.svg"
+1
View File
@@ -192,6 +192,7 @@ namespace montauk::abi {
static constexpr uint64_t SYS_SDR_READ = 146; // (handle, buf, len) -> bytes static constexpr uint64_t SYS_SDR_READ = 146; // (handle, buf, len) -> bytes
static constexpr uint64_t SYS_SDR_SETPARAM = 147; // (handle, param, value) static constexpr uint64_t SYS_SDR_SETPARAM = 147; // (handle, param, value)
static constexpr uint64_t SYS_SDR_GETPARAM = 148; // (handle, param) -> value static constexpr uint64_t SYS_SDR_GETPARAM = 148; // (handle, param) -> value
static constexpr uint64_t SYS_SETUNIXTIME = 153;
// Tunable parameters (for SYS_SDR_SETPARAM / SYS_SDR_GETPARAM). // Tunable parameters (for SYS_SDR_SETPARAM / SYS_SDR_GETPARAM).
static constexpr int SDR_PARAM_FREQ = 0; // center frequency, Hz static constexpr int SDR_PARAM_FREQ = 0; // center frequency, Hz
+5
View File
@@ -49,6 +49,7 @@ extern "C" {
#define MTK_SYS_GETARGS 25 #define MTK_SYS_GETARGS 25
#define MTK_SYS_RESET 26 #define MTK_SYS_RESET 26
#define MTK_SYS_SHUTDOWN 27 #define MTK_SYS_SHUTDOWN 27
#define MTK_SYS_SETUNIXTIME 153
#define MTK_SYS_GETTIME 28 #define MTK_SYS_GETTIME 28
#define MTK_SYS_SOCKET 29 #define MTK_SYS_SOCKET 29
#define MTK_SYS_CONNECT 30 #define MTK_SYS_CONNECT 30
@@ -533,6 +534,10 @@ static inline void mtk_gettime(mtk_datetime *out) {
_mtk_syscall1(MTK_SYS_GETTIME, (long)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) { static inline void mtk_settz(int offset_minutes) {
_mtk_syscall1(MTK_SYS_SETTZ, (long)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
@@ -351,6 +351,9 @@ namespace montauk {
// Timekeeping (wall-clock) // Timekeeping (wall-clock)
inline void gettime(montauk::abi::DateTime* out) { syscall1(montauk::abi::SYS_GETTIME, (uint64_t)out); } 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) // Timezone offset (total minutes from UTC)
inline void settz(int offset_minutes) { syscall1(montauk::abi::SYS_SETTZ, (uint64_t)(int64_t)offset_minutes); } inline void settz(int offset_minutes) { syscall1(montauk::abi::SYS_SETTZ, (uint64_t)(int64_t)offset_minutes); }