code wiki / (root) / ntp.nx

ntp.nx source

↩ module page · 193 lines · 7759 B

1// ntp.nx -- NTPv4 packet parser + builder (RFC 5905). 2// 3// Fixed 48-byte client/server packet over UDP port 123. Lets the 4// Nishi stack sync clock against a time server without ntpd, 5// chrony, or systemd-timesyncd. Essential for: 6// - file mtimes that survive reboots 7// - TLS certificate validity checks 8// - log timestamp ordering across machines 9// - obligation-ledger timestamps (nishi-prm) 10// 11// Packet layout (big-endian multi-byte): 12// 0 1 2 3 13// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 14// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 15// |LI | VN |Mode | Stratum | Poll | Precision | 16// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 17// | Root Delay | 18// +---------------------------------------------------------------+ 19// | Root Dispersion | 20// +---------------------------------------------------------------+ 21// | Reference ID | 22// +---------------------------------------------------------------+ 23// | Reference Timestamp (64) | 24// +---------------------------------------------------------------+ 25// | Origin Timestamp (64) | 26// +---------------------------------------------------------------+ 27// | Receive Timestamp (64) | 28// +---------------------------------------------------------------+ 29// | Transmit Timestamp (64) | 30// +---------------------------------------------------------------+ 31// 32// NTP 64-bit timestamps: seconds since 1900-01-01 00:00 UTC in 33// the upper 32 bits; fractional seconds in the lower 32 bits 34// (each tick = 1/2^32 seconds ≈ 232 picoseconds). 35// Unix epoch differs by 2208988800 seconds (NTP era 0). 36// 37// Mode values (LI=0, VN=4 typical): 38// 3 client 39// 4 server 40// 5 broadcast 41// 42// Invariants: 43// N1 Packet is exactly 48 bytes (extensions for key-ID+MAC 44// unused here). 45// N2 Timestamps returned as unix seconds (i64) + fractional 46// u32 packed in the low 32 bits when caller wants 47// sub-second precision. Simpler than exposing the raw 48// NTP 64-bit format. 49// N3 NTP_UNIX_EPOCH_DELTA = 2208988800 (seconds from 50// 1900-01-01 to 1970-01-01). 51 52import "syscalls.nx" 53 54const NTP_PACKET_SIZE: i64 = 48 55const NTP_UNIX_EPOCH_DELTA: i64 = 2208988800 56 57const NTP_MODE_CLIENT: i64 = 3 58const NTP_MODE_SERVER: i64 = 4 59const NTP_MODE_BROADCAST: i64 = 5 60 61const NTP_ERR_SHORT: i64 = -1 62 63struct NtpPacket { 64 li: i64, // leap indicator 0..3 65 version: i64, // typically 4 66 mode: i64, 67 stratum: i64, // 0 unspec, 1 primary, 2-15 secondary 68 poll: i64, // log2 poll interval 69 precision: i64, // signed i8, log2 precision 70 root_delay_u32: i64, // 16.16 fixed-point seconds 71 root_dispersion_u32: i64, 72 ref_id: i64, // 4-byte tag or IPv4 of upstream 73 ref_ts_unix: i64, // reference timestamp in unix seconds 74 ref_ts_frac: i64, // fractional part (u32) 75 orig_ts_unix: i64, 76 orig_ts_frac: i64, 77 recv_ts_unix: i64, 78 recv_ts_frac: i64, 79 tx_ts_unix: i64, 80 tx_ts_frac: i64, 81} 82 83// Read a big-endian u32 (returned as i64). 84func ntp_read_u32_be(buf: *u8, off: i64) -> i64 { 85 let b0: i64 = buf[off] 86 let b1: i64 = buf[off + 1] 87 let b2: i64 = buf[off + 2] 88 let b3: i64 = buf[off + 3] 89 return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3 90} 91 92// Write a big-endian u32. 93func ntp_write_u32_be(buf: *u8, off: i64, v: i64) -> i64 { 94 buf[off] = (v >> 24) & 0xFF 95 buf[off + 1] = (v >> 16) & 0xFF 96 buf[off + 2] = (v >> 8) & 0xFF 97 buf[off + 3] = v & 0xFF 98 return off + 4 99} 100 101// Read a 64-bit NTP timestamp at offset: returns two u32s in 102// *unix_sec_out (NTP seconds minus epoch delta) and *frac_out. 103func ntp_read_ts(buf: *u8, off: i64, 104 unix_sec_out: *i64, frac_out: *i64) -> i64 { 105 let ntp_secs: i64 = ntp_read_u32_be(buf, off) 106 *frac_out = ntp_read_u32_be(buf, off + 4) 107 if ntp_secs == 0 { 108 *unix_sec_out = 0 109 } else { 110 *unix_sec_out = ntp_secs - NTP_UNIX_EPOCH_DELTA 111 } 112 return 0 113} 114 115// Write a 64-bit NTP timestamp from unix seconds + fractional u32. 116func ntp_write_ts(buf: *u8, off: i64, unix_sec: i64, frac: i64) -> i64 { 117 var ntp_secs: i64 = 0 118 if unix_sec > 0 { ntp_secs = unix_sec + NTP_UNIX_EPOCH_DELTA } 119 ntp_write_u32_be(buf, off, ntp_secs) 120 ntp_write_u32_be(buf, off + 4, frac) 121 return off + 8 122} 123 124// Parse a 48-byte NTP packet. 125func ntp_parse(buf: *u8, n: i64, pkt: *NtpPacket) -> i64 { 126 if n < NTP_PACKET_SIZE { return NTP_ERR_SHORT } 127 let b0: i64 = buf[0] 128 pkt.li = (b0 >> 6) & 0x3 129 pkt.version = (b0 >> 3) & 0x7 130 pkt.mode = b0 & 0x7 131 pkt.stratum = buf[1] 132 pkt.poll = buf[2] 133 // precision is signed i8. 134 var prec: i64 = buf[3] 135 if prec >= 128 { prec = prec - 256 } 136 pkt.precision = prec 137 138 pkt.root_delay_u32 = ntp_read_u32_be(buf, 4) 139 pkt.root_dispersion_u32 = ntp_read_u32_be(buf, 8) 140 pkt.ref_id = ntp_read_u32_be(buf, 12) 141 142 ntp_read_ts(buf, 16, (pkt as i64 + 112) as *i64, (pkt as i64 + 120) as *i64) 143 ntp_read_ts(buf, 24, (pkt as i64 + 128) as *i64, (pkt as i64 + 136) as *i64) 144 ntp_read_ts(buf, 32, (pkt as i64 + 144) as *i64, (pkt as i64 + 152) as *i64) 145 ntp_read_ts(buf, 40, (pkt as i64 + 160) as *i64, (pkt as i64 + 168) as *i64) 146 return 0 147} 148 149// Build a minimal NTP client request packet. Caller fills 150// tx_timestamp (which the server echoes back as origin_ts so the 151// client can compute round-trip delay + offset). 152func ntp_build_client(buf: *u8, cap: i64, 153 tx_unix_sec: i64, tx_frac: i64) -> i64 { 154 if cap < NTP_PACKET_SIZE { return NTP_ERR_SHORT } 155 var i: i64 = 0 156 while i < NTP_PACKET_SIZE { buf[i] = 0; i = i + 1 } 157 // LI=0, VN=4, Mode=3 (client): 0x23. 158 buf[0] = 0x23 159 buf[1] = 0 // stratum 0 (client) 160 buf[2] = 4 // poll interval log2 (16s) 161 buf[3] = 0xEC // precision ~= -20 (i8) 162 // Root delay + dispersion left zero. 163 // Reference ID left zero. 164 // Timestamps: only transmit timestamp matters for a query. 165 ntp_write_ts(buf, 40, tx_unix_sec, tx_frac) 166 return NTP_PACKET_SIZE 167} 168 169// Compile-only smoke: build a client packet + round-trip parse. 170func main() -> i64 { 171 let out: *u8 = sys_mmap(64) 172 // 2026-04-22 some time; real unix seconds not critical. 173 let tx_sec: i64 = 1777000000 174 let tx_frac: i64 = 0x80000000 // half-second fractional 175 176 let n: i64 = ntp_build_client(out, 64, tx_sec, tx_frac) 177 if n != NTP_PACKET_SIZE { return 1 } 178 179 // Verify LI/VN/Mode byte. 180 if out[0] != 0x23 { return 2 } 181 182 // Parse it back. 183 let pkt_raw: *u8 = sys_mmap(256) 184 let pkt: *NtpPacket = pkt_raw as *NtpPacket 185 if ntp_parse(out, NTP_PACKET_SIZE, pkt) != 0 { return 3 } 186 if pkt.li != 0 { return 4 } 187 if pkt.version != 4 { return 5 } 188 if pkt.mode != NTP_MODE_CLIENT { return 6 } 189 if pkt.tx_ts_unix != tx_sec { return 7 } 190 if pkt.tx_ts_frac != tx_frac { return 8 } 191 192 return 0 193}