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=1) NTP unreachable → warning, continue (network may not allow UDP/123 outbound)

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) #

stable0.7.30
{
  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() #

stable0.7.30

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) #

stable0.0.7
{
  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.

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) #

stable0.0.7
{
  servers:   ["time.cloudflare.com", "pool.ntp.org"],
  port:      123,
  timeoutMs: 3000,
}

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.

var result = await b.ntpCheck.checkDrift({
  servers: ["time.cloudflare.com", "pool.ntp.org"],
});
// → { driftMs: 12, serverTimeMs: 1714694400000, server: "time.cloudflare.com" }

b.ntpCheck.bootCheck(opts) #

stable0.0.7
{
  servers:      ["time.cloudflare.com", "pool.ntp.org"],
  port:         123,
  timeoutMs:    3000,
  driftWarnMs:  300000,    // override registered warn threshold
  driftFatalMs: 3600000,   // override registered fatal threshold
}

Boot-time clock-drift check that integrates with the framework's logging policy. Resolves with { ok, severity, driftMs, server, message } where severity is info / warning / fatal. The framework's b.db.init calls this and refuses to boot when ok === false and the operator has set BLAMEJS_NTP_STRICT=1. NTP unreachable returns severity: "warning" (network may not allow UDP/123 outbound) so the boot doesn't fail closed without operator intent.

var result = await b.ntpCheck.bootCheck({
  servers:      ["time.cloudflare.com"],
  driftWarnMs:  60000,
  driftFatalMs: 600000,
});
// → { ok: true, severity: "info", driftMs: 12,
//     server: "time.cloudflare.com",
//     message: "clock drift +12ms from time.cloudflare.com" }

b.ntpCheck.monitor(opts) #

stable0.7.30
{
  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
}

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.

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();

Last updated 2026-08-08T16:39:49.652Z by seeder.