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}