Repository navigation
w698: every critical unit in the portal VM starts after a reboot; fff-health restarts any that is down - #235
Merged
Merged
Conversation
… fff-health restarts any that is down fff-ops.socket was ordered After= and Requires= fff-ops-scratch.service: an ordering cycle at boot (a socket is before sockets.target, which is before basic.target; a service is after it), so systemd deleted a job to break it and never started the door. Every fffctl update restarted it by hand, which hid the bug until a reboot. - fff-ops.socket orders and requires nothing; fff-ops@.service already requires the scratch file system. - One list of critical units (FFF_CRITICAL_UNITS): install.sh enables all of them and fails if one is not enabled. - Restart=on-failure with StartLimit on the firewall and scratch units, a tailscaled drop-in; the units install.sh wrote from heredocs are templates (units/*.in) now, so they are linted. - fff-watchdog (run by fff-health every 30 s): restarts a failed or stopped critical unit, sockets included, enables a disabled one, with a back-off, a stop after 6 restarts that did not hold, a log line for every restart and data/unit-watchdog.json; fffctl status / units / watchdog, and the portal's banner and system_status show it. - fff-vm watch: a new layer for the VM's units; layer 7 counts only dropped TCP from the Funnel servers. - fffctl status: the "not joined" line now comes from Tailscale's backend state, not the fall-back of a failed pipe. - Tests: fff-units.test.sh (WantedBy, enable, Restart=, no socket after a service, systemd-analyze on the boot graph, which fails the old socket), fff-watchdog.test.sh, the path-watch fixtures, and the CI VM end to end. Request: w698 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… and warns when it grows while the outside probe fails Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… at cycles through fff units Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… own sudo log line Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This was referenced Oct 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Request: w698
TL;DR:
fff-ops.socketdid not come back after the VM's cold restart because it was ordered after a service, which is an ordering cycle at boot: systemd deletes a job to break it and the socket never starts. A deploy (install.shrestarts it by hand) hid this until a reboot. This PR removes the cycle, makes every critical unit enabled and checked at install, adds a unit watchdog tofff-healththat restarts any failed or stopped critical unit (with back-off, a log line and a record thatfffctl statusand the portal show), and fixes the two warnings lothsahn saw (not joined, layer 7). Needs a portal deploy (fffctl update).Root cause
deploy/vm/guest/units/fff-ops.sockethadAfter=andRequires=fff-ops-scratch.service. A socket unit isBefore=sockets.target,sockets.targetis beforebasic.target, and a normal service isAfter=basic.target: basic.target > sockets.target > fff-ops.socket > fff-ops-scratch.service > basic.target.SYSTEMD_UNIT_PATH=<units> systemd-analyze verify default.target):basic.target: Found ordering cycle on sockets.target/start,... Found dependency on fff-ops.socket/start,... Found dependency on fff-ops-scratch.service/start,... Job sockets.target/start deleted to break ordering cycle starting with basic.target/start. The fixed unit prints nothing.deploy/vm/test/fff-units.test.shruns this check on the boot graph and also proves it fails the old socket.install.shends withsystemctl restart fff-ops.socketwhenever it is not active, and a restart after boot has no cycle to break: eachfffctl updaterevived the door, each reboot killed it.Requires=also stopped the socket wheneverfff-ops-scratch.servicewas stopped or restarted.What changes
fff-ops.socket: no ordering on any service (the per-connectionfff-ops@.servicealready requires the scratch).FFF_CRITICAL_UNITS(lib.sh): firewall, tailscaled, scratch, socket, portal, update path, health/backup/base-refresh timers.install.shenables the whole list and dies if one is not enabled after.tailscaledis enabled explicitly (it relied on the package postinst).Restart=on-failure+StartLimit*onfff-guest-firewallandfff-ops-scratch;tailscaleddrop-in (Restart=on-failure, 5 s, 10 starts / 10 min).fff-ops-scratch,fff-ops@andfff-backup.timerwere heredocs in install.sh; nowunits/*.intemplates (render_unit), so they are linted.fff-watchdog(run byfff-healthevery 30 s): a critical unit that is failed, inactive when it should run, or active without doing its job (no nftables table, not mounted, no socket file) is restarted; a disabled one is enabled. Back-off 30 s doubling to 900 s, stops after 6 restarts that did not hold (dispatcher told once), starts over after 600 s healthy. Skips: VM shutting down, unit starting/stopping or with a job,fffctl watchdog pause, the held portal. Every restart: journal tagfff-watchdoganddata/unit-watchdog.json.fffctl statusprints the unit table and the last restarts (unit, when, why);fffctl units [--check],fffctl watchdog run|pause|resume|reset. The portal shows a banner for a unit it gave up on, a dismissible one for the last 24 h of restarts, andsystem_statuslines (server/unitWatchdog.ts,web/src/App.tsx).fff-vm watch(host): new layerunits(fffctl units --check), the check on the watchdog from outside (it cannot restart its own timer). Hostinstall.shverifies its units and the VM's autostart are enabled.fffctl status"not joined": was the|| echo 'not joined'fall-back oftailscale status --self | head -n 1, which fires whenever that pipe fails under pipefail, so a joined node printed its address and thennot joined. Now reads Tailscale'sBackendState. (Which of the two failure causes hit lothsahn's run needs the VM; the old code cannot say it means "not joined".)fff-vm watchlayer 7: counts only dropped TCP fromtag:ingresspeers (Funnel is TCP; ICMP/UDP drops are shown, never judged), and is a warning with the ports while the outside probe passes. Message of the failing case unchanged.Evidence
deploy/vm/test/fff-watchdog.test.sh(new, 11 scenarios, fake systemctl): all pass in Ubuntu 24.04 (WSL).deploy/vm/test/fff-units.test.sh(new): passes; fails with the old socket (checked).fff-vm-path.test.sh46 passed,fff-vm-nightly.test.sh9,fff-machine-ssh.test.sh7; shellcheck 0.10-S warningclean on all touched scripts;npm run typecheck(root + web) clean;server/unitWatchdog.test.ts+ pathHealth + secretGuard 46 pass.lint.sh --units(root, throwaway machine) and the nested-VM end to end (new step: after the nightly cold restart every unit is up, no ordering cycle in the boot journal, a stoppedfff-ops.socketand a disabledfff-update.pathare put right). CI runs both.After the merge
Waiting for lothsahn's deploy (
fffctl update; it can carry #234, w694). Verification steps are in the w698 report.🤖 Generated with Claude Code