NTP Check
Boot-time clock-drift verification against an external NTP / NTS-KE reference. The audit chain's monotonicCounter orders events deterministically even when the wall clock jumps, but recordedAt is the human-readable timestamp auditors rely on — a clock silently off by hours (container with no RTC sync, NTP daemon stopped) makes the audit trail misleading without ever surfacing as an error.
What this does: sends a single SNTPv4 query over UDP/123 (RFC 5905) to one or more configured servers, computes drift as serverTransmit - localMidpoint (round-trip-corrected), returns the drift in milliseconds. Falls through a server list in order; the first success wins.
What this does NOT do: continuous synchronization (the host OS's NTP daemon does that), authenticated NTP / NTS / autokey (the external reference is trust-on-first-query), or median-of-N server reconciliation (single-shot only).
Policy thresholds at boot — wired into b.db.init:
drift |x| < warnMs (5 min default) → info, continue drift |x| in [warnMs, fatalMs) → warning, continue drift |x| >= fatalMs (1 hr default) → refuse to boot (BLAMEJS_NTP_STRICT=0 downgrades to a log line) NTP unreachable → warning, continue (network may not allow UDP/123 outbound; BLAMEJS_NTP_REQUIRE_REACHABLE=1 refuses the boot instead)
b.ntpCheck.monitor runs the same check on a recurring interval after boot and emits system.ntp.checked / system.ntp.drift_warn / system.ntp.drift_fatal / system.ntp.unreachable audit events plus an ntp.drift_ms observability gauge — so silent clock drift mid-flight surfaces in the same evidence stream as boot drift.
b.ntpCheck.setThresholds(opts) #
{
warnMs: 300000, // ms; absolute drift at-or-above this logs warn
fatalMs: 3600000, // ms; absolute drift at-or-above this refuses boot
}
Override the warn / fatal drift thresholds applied by bootCheck and monitor. Validates that both values are non-negative finite numbers and that warnMs <= fatalMs (a fatal floor below the warning threshold would mean every warning is also fatal — likely a typo). Throws TypeError on bad shapes and RangeError on the ordering invariant.
b.ntpCheck.setThresholds({
warnMs: 60000,
fatalMs: 600000,
});
var t = b.ntpCheck.getThresholds();
// → { warnMs: 60000, fatalMs: 600000 }
b.ntpCheck.getThresholds() #
Read the currently-effective warn / fatal drift thresholds. Returns a fresh object so mutating the result doesn't accidentally rewrite framework state.
var t = b.ntpCheck.getThresholds();
// → { warnMs: 300000, fatalMs: 3600000 }
b.ntpCheck.querySingle(server, opts) #
{
port: 123, // UDP port (almost always 123)
timeoutMs: 3000, // single-query timeout
}
Send one SNTPv4 query to a named server over UDP/123 and resolve with { driftMs, serverTimeMs, server } (round-trip-corrected drift). Rejects with { code, message } where code is one of ntp/timeout (no reply within timeoutMs), ntp/refused (DNS / connection error), ntp/bad-reply (packet too short), or ntp/unsynchronized (Stratum-16 peer with zero transmit timestamp). IPv4 / IPv6 socket family is selected from the host literal so an fd00::... server doesn't fail with EINVAL.
A port outside 1-65535 throws ntp/bad-port synchronously, before the socket opens, rather than rejecting like the codes above.
// requires: outbound NTP (udp/123) to the server named below
b.ntpCheck.querySingle("time.cloudflare.com", { timeoutMs: 2000 })
.then(function (r) { console.log("drift", r.driftMs, "ms"); })
.catch(function (e) { console.error("ntp", e.code, e.message); });
b.ntpCheck.checkDrift(opts) #
{
servers: ["time.cloudflare.com", "pool.ntp.org"],
ntsServers: ["time.cloudflare.com", "nts.example:4460", "[2001:db8::1]:4460"], // array; host, host:port or [v6]:port — tried first, authenticated (RFC 8915)
requireNts: false, // boolean; no unauthenticated fallback when true
port: 123,
timeoutMs: 3000,
driftWarnMs: 2000, // accepted and ignored here; carried so one config object
driftFatalMs: 30000, // passes to b.ntpCheck.monitor, which reads them
intervalMs: 900000, // (checkDrift measures drift; it does not judge or
audit: true, // schedule it, and the caller deciding that is
onDrift: function (result) {}, // typically bootCheck or monitor)
Opts are validated against a schema. A field present with the wrong type is
refused rather than read as absent: a string `requireNts` would otherwise
drop the authenticated reading, a non-array `ntsServers` would attempt no
authenticated query at all, and a string `servers` has a length and indexes,
so it was walked CHARACTER BY CHARACTER — the configured server never
contacted, the result an ordinary unreachable warning. A key the schema does
not declare is refused too, so a typo cannot be silently consumed;
`bootCheck` and `monitor` forward their own opts through here and those are
allowed, validated where they are read.
}
Walk a server list in order; resolve with the first successful drift measurement ({ driftMs, serverTimeMs, server }). When every server in the list fails, resolves with { driftMs: null, error } so the caller — typically bootCheck — can decide whether unreachable NTP is fatal or a soft warning.
The result says which kind of answer it is: authenticated: true when it came from an NTS server (RFC 8915), false from plain SNTP. Named NTS servers are tried first; requireNts makes a failed authenticated query the answer rather than a reason to fall back to an unauthenticated one.
// requires: outbound NTP (udp/123) to the servers named below
var result = await b.ntpCheck.checkDrift({
servers: ["time.cloudflare.com", "pool.ntp.org"],
});
// → { driftMs: 12, serverTimeMs: 1714694400000,
// server: "time.cloudflare.com", authenticated: false }
b.ntpCheck.bootCheck(opts) #
{
servers: ["time.cloudflare.com", "pool.ntp.org"],
port: 123,
timeoutMs: 3000,
driftWarnMs: 300000, // override registered warn threshold
driftFatalMs: 3600000, // override registered fatal threshold
ntsServers: ["time.cloudflare.com:4460"], // NTS-KE endpoints tried BEFORE the plain `servers`,
// each "host", "host:port" or "[v6]:port"; a malformed entry raises
// ntp/bad-endpoint. A reading from one of these carries
// authenticated: true
requireNts: false, // with it true, the plain `servers` are not consulted at all, so if
// every NTS endpoint fails the result is { driftMs: null,
// authenticated: false } carrying the last error rather than an
// unauthenticated reading
intervalMs: 900000, // accepted and ignored here; carried so one config object passes to
audit: true, // b.ntpCheck.monitor, which reads them. bootCheck takes a single
onDrift: function (result) {}, // reading at startup and does not schedule or hook
}
Boot-time clock-drift check that integrates with the framework's logging policy. Resolves with { ok, severity, driftMs, server, authenticated, message } where severity is info / warning / fatal and authenticated says whether the reading came from an authenticated (RFC 8915) server or a plain SNTP reply anyone on the path could have written. The framework's b.db.init calls this. Drift past the fatal threshold refuses the boot by default, and BLAMEJS_NTP_STRICT=0 downgrades that refusal to a log line.
Opts are forwarded to checkDrift, so ntsServers and requireNts apply here as well, and they are validated against a schema: a field present with the wrong type is refused rather than ignored. That includes the two threshold overrides, which were read with a typeof test and silently dropped when they were not numbers — the boot then ran against the registered threshold rather than the one passed. Zero remains valid on both: it disables that threshold.
An unreachable server is a different question and answers severity: "warning" with ok: true, because a network that does not allow outbound UDP/123 is ordinary and refusing every such host by default would be a boot failure nobody asked for. An operator who does want it refused sets BLAMEJS_NTP_REQUIRE_REACHABLE=1, which bootCheck reads: the same unanswered query then answers ok: false with severity: "fatal", and b.db.init turns that into db/ntp-unreachable. Its own refusals are ntp/bad-opts for an option of the wrong type or a name it does not accept, and ntp/bad-port for a port outside 1-65535. It is the only setting that turns an unanswered query into a refusal, and BLAMEJS_NTP_STRICT has no bearing on it. Either way the result carries unreachableReason, the reason no reading was obtained.
// requires: outbound NTP (udp/123) to the servers named below
var result = await b.ntpCheck.bootCheck({
servers: ["time.cloudflare.com"],
driftWarnMs: 60000,
driftFatalMs: 600000,
});
// → { ok: true, severity: "info", driftMs: 12,
// server: "time.cloudflare.com", authenticated: false,
// message: "clock drift +12ms from time.cloudflare.com" }
b.ntpCheck.monitor(opts) #
{
intervalMs: 900000, // tick cadence
servers: ["time.cloudflare.com", "pool.ntp.org"],
driftWarnMs: 2000,
driftFatalMs: 30000,
audit: true, // emit audit events
onDrift: function (result) {}, // operator hook
port: 123, // UDP port for the plain `servers`; a value
// outside 1-65535 raises ntp/bad-port
timeoutMs: 3000, // per-query ceiling, applied to an NTS endpoint
// and a plain server alike
ntsServers: ["time.cloudflare.com:4460"], // NTS-KE endpoints tried BEFORE the plain
// `servers`, each "host", "host:port" or "[v6]:port"; a malformed entry
// raises ntp/bad-endpoint. A reading from one carries authenticated: true
requireNts: false, // with it true, the plain `servers` are not
// consulted at all, so if every NTS endpoint fails the tick reports
// { driftMs: null, authenticated: false } with the last error rather
// than an unauthenticated reading
}
Periodic drift monitor — runs bootCheck on a recurring interval and emits audit + observability events on threshold crossings. Returns a handle with .stop() for graceful shutdown. Audit emissions: system.ntp.checked on every tick, system.ntp.drift_warn and system.ntp.drift_fatal on threshold crossings, system.ntp.unreachable when every server in the list failed. Observability gauge ntp.drift_ms rides every successful check. The optional onDrift hook fires only when severity is warning or fatal, so operators can page on drift without inspecting every healthy tick.
// requires: outbound NTP (udp/123) to the servers named below
var mon = b.ntpCheck.monitor({
intervalMs: 900000,
servers: ["time.cloudflare.com", "pool.ntp.org"],
driftWarnMs: 2000,
driftFatalMs: 30000,
onDrift: function (r) { console.warn("ntp drift", r.driftMs); },
});
await mon.stop();