Rollback Guard¶
internal/rollback. A config change that cuts the device off must undo itself,
because nobody can reach the device to undo it by hand.
Applying a config arms a timer. If the platform does not confirm the version before it expires, the device puts itself back.
The shape¶
guard := rollback.NewGuard(pfApplier, dataDir, 60*time.Second, logger) // serve.go
err := guard.Arm(rollback.CaptureSnapshotWith(pfApplier, ifApplier), version)
guard.Confirm(version) // cancel the timer, discard the snapshot
guard.IsPending() bool
The trial window is 60 seconds, fixed in cmd/agent/serve.go. The portal's
countdown is driven by Nexus; the device only waits for confirm_config.
Arm returns an error, and the caller does not apply if it does. Arming
writes the snapshot to disk (pf.conf.rollback and pending.json in the data
directory, mode 0600, because the snapshot can hold the IPsec pre-shared key).
A change that could not be undone is never made.
What a snapshot holds¶
| Field | Why |
|---|---|
PFConf |
The firewall ruleset |
Mygate, DefaultGateway |
The management path: a restored ruleset is useless if the route out is gone |
Files |
Other generated files, each with its content, whether it existed, and how to reload it |
VirtualInterfaces |
Destroyable interfaces present before the change |
Files covers /etc/iked.conf, /etc/dhcpd.conf,
/var/unbound/etc/unbound.conf, /etc/dhcpleased.conf, /etc/ifstated.conf
and every /etc/hostname.<if> (except lo, enc, pflog).
A file that did not exist is removed, not emptied. Restoring an empty string is not the same as removing a file the failed config created.
VirtualInterfaces is nil when nothing was inspected, and that is
deliberate: nil means "never looked" and is skipped; an empty slice means
"looked, there were none" and is acted on. Confusing the two would destroy
interfaces on a device the guard knows nothing about.
The guard restores the kernel as well as files because a rolled-back change can
otherwise leave something running that the platform believes is gone: a tunnel
whose config was removed, a wg0 described by no configuration, a route that
broke connectivity. The rule is that whatever the failed version created, in a
file or in the kernel, is taken away again.
On expiry¶
The guard (rollback.go):
- restores
pf.confand reloads it, so filtering is back first; - writes
/etc/mygateand resets the kernel default route; - destroys virtual interfaces created since the snapshot, before restoring
files, so a restored
hostname.ifcannot collide with an address still held by the interface being removed; - restores the generated files and reloads their daemons (
rcctl reload iked, restartdhcpd/dhcpleased/ifstated, reloadunbound,sh /etc/netstart <if>). A daemon that will not reload does not stop the rest; a service that is switched off is logged as routine.
Then the SetOnRollback callback in serve.go:
- restores the config store to the previous version and updates live state;
- reconciles static routes against the abandoned config, removing what it added;
- sends a rollback report to Nexus.
Surviving a reboot¶
pending.json holds {version, deadline, snapshot}. At startup, before the
connector runs, ResumePending():
- with no record, does nothing;
- with a deadline already passed, rolls back immediately;
- with a deadline ahead, re-arms for the remaining time.
A malformed record is discarded rather than treated as fatal. A box that rebooted mid-trial therefore still rolls back an unconfirmed change.
Confirming¶
Confirm(version) cancels the timer and discards the snapshot. A confirm for a
different version than the armed one is logged (confirm version mismatch,
accepting anyway) and still accepted.