Skip to content

Instantly share code, notes, and snippets.

@rotu
Last active December 4, 2024 18:05
Show Gist options
  • Save rotu/596894708631ab9800303373b9e79ce4 to your computer and use it in GitHub Desktop.
Save rotu/596894708631ab9800303373b9e79ce4 to your computer and use it in GitHub Desktop.
import dgram from "node:dgram"
import dnsPromises from "node:dns/promises"
import timersPromises from "node:timers/promises"
addEventListener("error", (event) => {
// reportError(event.error)
console.error("Caught unhandled event:", event.message)
// event.preventDefault()
})
const sock = dgram.createSocket("udp4")
await new Promise((resolve) =>
sock.bind({ port: 0, address: "0.0.0.0" }, resolve)
)
const UNIX_EPOCH = new Date(0).getTime()
const NTP_EPOCH = new Date("1900-01-01").getTime()
const PERF_EPOCH = performance.timeOrigin
const NTS_PER_SEC = (2 ** 32)
const NTS_PER_MSEC = NTS_PER_SEC / 1000
const PERF_EPOCH_NTS = BigInt(
Math.round((PERF_EPOCH - NTP_EPOCH) * NTS_PER_MSEC),
)
function ntsDiffSeconds(n1, n0) {
console.assert(typeof n1 === 'bigint')
console.assert(typeof n0 === 'bigint')
return (Number(n1 - n0) / NTS_PER_SEC)
}
function ntsPlusMillis(n, d) {
console.assert(typeof n === 'bigint')
console.assert(typeof d === 'number')
return n + BigInt(Math.round(d * NTS_PER_MSEC))
}
function perfToNts(p) {
return ntsPlusMillis(PERF_EPOCH_NTS, p)
}
function fp2d(r) {
return r / 65536
}
function d2fp(r) {
return Math.round(r * 65536)
}
const FRAC = 2 ** 32
const LFP2D = (a) => Number(a) / FRAC
function LOG2D(a) {
return 2 ** a
}
function ntpToDate(n) {
if (n === 0n) {
return null
}
return new Date(Number(n) / NTS_PER_MSEC + NTP_EPOCH)
}
const NOSYNC = 3
// figure 6: Global Parameters
const DEFINES = {
PORT: 123,
VERSION: 4,
TOLERANCE: 15e-6,
MINPOLL: 4,
MAXPOLL: 17,
MAXDISP: 16,
MINDISP: .005,
MAXDIST: 1,
MAXSTRAT: 16
}
// System structure
const s = {
leap: NOSYNC, // nosync
precision: -13, // a millisecond
stratum: DEFINES.MAXSTRAT,
poll: DEFINES.MINPOLL,
reftime: perfToNts(performance.now()),
refid: 0
}
await (new Promise((resolve) => setTimeout(resolve, 5000)))
const associations = []
// see mobilize
async function initAssociations() {
const dnsResults = await dnsPromises.lookup("time.google.com", {
family: 4,
all: true,
})
for (const { address } of dnsResults) {
associations.push({
// config
srcaddr: address,
version: 4,
hmode: 3,
hpoll: DEFINES.MINPOLL
})
}
}
await initAssociations()
function clear(peer, kiss) {
p.leap = NOSYNC
p.stratum = DEFINES.MAXSTRAT
p.ppoll = DEFINES.MAXPOLL
p.hpoll = DEFINES.MINPOLL
p.disp = DEFINES.MAXDISP
p.jitter = LOG2(s.precision)
p.refid = kiss
}
// NTP = https://datatracker.ietf.org/doc/html/rfc5905
// SNTP = https://datatracker.ietf.org/doc/html/rfc4330
async function peer_xmit(peer) {
const xmt = perfToNts(performance.now())
const packet = new Uint8Array(
48
// //extension fields are variable
// 32 + 128,
)
const dv = new DataView(packet.buffer)
// Figure 7: Packet Header Variables
// const stratum = 0,
// poll = 6,
// precision,
// rootdelay, rootdisp, refid, reftime, org, rec, xmt, dst, keyid, dgst;
dv.setUint8(0, (s.leap << 6) | (peer.version << 3) | (peer.hmode << 0))
dv.setUint8(1,
s.stratum === DEFINES.MAXSTRAT ? 0 : s.stratum) // stratum
dv.setUint8(2, peer.hpoll) // poll
dv.setUint8(3, s.precision) // precision
dv.setUint32(4, 0) // root delay
dv.setUint32(8, 0) // root dispersion
dv.setUint32(12, s.refid) // reference id
dv.setBigUint64(16, s.reftime) // reference timestamp
dv.setBigUint64(24, 0n) // origin timestamp
dv.setBigUint64(32, 0n) // receive timestamp
dv.setBigUint64(40, xmt) // transmit timestamp
sock.send(packet, DEFINES.PORT, peer.srcaddr)
}
function recv_packet(msg, rinfo) {
const now = performance.now()
const dst = perfToNts(now)
const dv = new DataView(msg.buffer, msg.byteOffset, msg.byteLength)
const b0 = dv.getUint8(0)
// this is the struct r
return {
srcaddr: rinfo.address,
version: (b0 >> 3) & ((1 << 3) - 1),
leap: (b0 >> 6) & ((1 << 2) - 1),
mode: (b0 >> 0) & ((1 << 3) - 1),
stratum: dv.getUint8(1),
poll: dv.getUint8(2),
precision: dv.getUint8(3),
rootdelay: dv.getUint32(4),
rootdisp: dv.getUint32(8),
refid: dv.getUint32(12),
reftime: dv.getBigUint64(16),
org: dv.getBigUint64(24),
rec: dv.getBigUint64(32),
xmt: dv.getBigUint64(40),
dst,
}
}
function packet(p, r) {
// update the peer info
p.leap = r.leap
p.stratum = r.stratum || DEFINES.MAXSTRAT
p.pmode = r.mode
p.ppoll = r.poll
p.rootdelay = fp2d(r.rootdelay)
p.rootdisp = fp2d(r.rootdisp)
p.refid = r.refid
p.reftime = r.reftime
if (p.leap == NOSYNC || p.stratum >= DEFINES.MAXSTRAT) {
console.warn("unsynchronized")
return
}
if (p.rootdelay / 2 + r.rootdisp >= DEFINES.MAXDISP || p.reftime > r.xmt) {
console.warn("invalid header values")
return
}
p.reach = true
// compute some statistics
let offset, delay
offset = (ntsDiffSeconds(r.rec, r.org) + ntsDiffSeconds(r.dst, r.xmt)) / 2
delay = Math.max(ntsDiffSeconds(r.dst, r.org) - ntsDiffSeconds(r.rec, r.xmt), LOG2D(s.precision))
console.log({ offset, delay }, r.srcaddr)
}
function find_assoc(r) {
return associations.find(peer => r.srcaddr === peer.srcaddr)
}
sock.on("message", (msg, rinfo) => {
const r = recv_packet(msg, rinfo)
const peer = find_assoc(r)
if (!peer) {
console.warn("no peer found for received message. Ignoring...")
return
}
// update origin and destination timestamps
peer.org = r.xmt
peer.rec = r.dst
packet(peer, r)
})
for (let i = 0; i < 10; ++i) {
console.log('--------')
for (const p of associations) {
await peer_xmit(p)
}
await timersPromises.setTimeout(8000)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment