Skip to main content

Module clock

Module clock 

Source
Expand description

The fourth of those jobs, added 2026-09-14: the wall clock. The appliance booted two hours fast and refused every genuine credential and every Stripe webhook for it; UDP/123 is closed on that network, so “now” arrives in the Date header of an HTTPS response, from three sources that must agree. The appliance’s wall clock: glue between PID 1 and edda estate-clock.

Every rule — the sources, the quorum, the sanity window, the signed-peer protocol, step-at-boot and slew-while-running, the first fed syncs and the running triggers — lives in edda’s estate-clock crate (where Gunnar’s own gunnar-clock moved, lane L10 2026-10-21), which the front’s gunnar-front links too. This file decides only whether this process owns the clock, where its feed is, and what it says about the answer.

Both wires during the move: a box asks in ESTATE-TIME/1 and asks again in GUNNAR-TIME/1 when an old front answers in Gunnar’s dialect (estate_clock::peer::query); GUNNAR_CLOCK* (the kit’s korp.env.GUNNAR_CLOCK=) is read as an alias of ESTATE_CLOCK* (estate_clock::config::var).

§What went wrong with the first cut (gunnar 5792dda), measured

The appliance booted two hours fast; 5792dda asked three internet HTTPS hosts for a Date at boot and was re-imaged on 2026-09-14 — and the box still refused credentials 7199529 ms away. Through the control plane, with a credential deliberately signed 7 199 500 ms fast so the gate let it through, gunnar control mirror add … https://github.com/… answered github.com could not be resolved: failed to lookup address information: Try again. UpCloud’s firewall catch-all drop drops the replies to outbound UDP (from the front: dig +notcp times out, dig +tcp answers), musl’s resolver asks over UDP only, so every source was unreachable, the verdict was “no quorum”, and the clock was left alone. The only record was a log line on a VGA console. See the estate-clock crate note for the full measurement.

§What it does now

  • The source is configuration (korp.env.GUNNAR_CLOCK= on the kernel command line). On gunnar.rs that is peer://10.13.8.99:50123 — the front, by IP literal over the utility network, answering a nonce with a signed timestamp (GUNNAR_CLOCK_KEY names its public key). No internet, no DNS. Unset means not configured, and nothing is asked: an appliance in an airgap has no business dialling Stripe for the time.
  • At boot, after the network and before any listener: one sync, steps.
  • While running, only when a credential is refused for skew (rate-limited to one sync a minute) and on a 6-hour safety timer — estate_clock::trigger. Small offsets slew, large ones step — estate_clock::adjust.
  • Every attempt is recorded (estate_clock::report) and served on the control plane: Admin.Health carries the boot and the last report, and Bootstrap.Status — the one call that needs no credential — carries the last one and this server’s own clock. When the clock is wrong a credential is exactly what gets refused, so the credential-free call is the one that makes a wrong clock diagnosable from the front.

§Why it can never stop the boot or the server

Every function here returns () and swallows everything. A forge that refuses to come up because a clock fetch failed is strictly worse than a forge with a wrong clock: the wrong clock refuses credentials, the dead forge refuses everything — including the call an operator uses to find out why. The periodic sync runs on the blocking pool, never on a request path.

Constants§

NO_HTTPS
The refusal an https:// clock source gets in a build without clock-fetch.

Functions§

arm_triggers
The running triggers: a credential refused for skew, and the 6-hour safety timer (estate_clock::trigger::arm has the rules and why there are only these, and arms them once per process). Called after the boot sync, and by a fed run before its first pause. Does nothing on a box that does not own its clock, or whose clock is not configured — a clock the installer’s door feeds later arms them then (fed_arrived).
fed_arrived
The installer’s door fed a clock source or key while this box runs (the pairing loop’s first answer, crate::feed). Every sync already reads the feed (crate::feed::clock_config), so this only makes the first one happen NOW — a first boot whose medium carries no clock would otherwise wait for the 6-hour timer, which was never armed — and arms the triggers.
last_outcome
The outcome of this process’s latest clock attempt (crate::report::last), None before any.
outcome_is_right
Whether outcome left the clock right: set (stepped or slewed) or found right already. None (no attempt yet) is not right.
synchronise_at_boot
The boot call. Sync once, stepping if the source says so.
tunnel_up
This box’s tunnel just came up (the pairing loop’s TunnelUp).