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 ispeer://10.13.8.99:50123— the front, by IP literal over the utility network, answering a nonce with a signed timestamp (GUNNAR_CLOCK_KEYnames 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.Healthcarries the boot and the last report, andBootstrap.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 withoutclock-fetch.
Functions§
- arm_
triggers - The running triggers: a credential refused for skew, and the 6-hour
safety timer (
estate_clock::trigger::armhas 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),Nonebefore any. - outcome_
is_ right - Whether
outcomeleft 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).