Skip to content

Upgrade in place, depend on redis, and summarise each scan - #14

Merged
andylemin merged 1 commit into
masterfrom
installer-upgrades
Sep 27, 2026
Merged

andylemin merged 1 commit into
masterfrom
installer-upgrades

Conversation

@andylemin

@andylemin andylemin commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Four operational changes: the installers upgrade in place, the sync loop reports the scans that do work, the rc.d scripts start Redis, and the blocklist tool uses the HaGeZi lists the README recommends.

Upgrades

Both server installers put the service back as they found it. rcctl check records whether it was running, the daemon is stopped before anything is replaced, and started again only if it had been running.

This was a silent failure, not a missing convenience: OpenBSD's install(1) renames into place, so replacing the binary under a running daemon succeeds and that daemon keeps executing the old inode. An upgrade printed "All Completed successfully" and changed nothing until the next manual restart.

The Rust installer stops only once its build has succeeded, so a failed build leaves the old daemon serving. A failed restart is reported as a failure, because the firewall was whitelisting a moment before. A daemon the operator had stopped stays stopped, and only then does the installer print its enable and start advice.

Two new passes in the OpenBSD job cover both cases: re-running the installer against a running daemon must stop it, replace it and start it, with the process ids proving the restart and the config proving it was untouched; against a stopped daemon it must leave it stopped.

One line per scan that does something

The sync loop logs the table's size, what expired, and what was added to or removed from the table and the persist file:

pfui_ipv4_domains: 142 addresses, 3 expired, table +1/-0, file +1/-0

A cycle that changed nothing logs nothing, so INFO stays quiet in a steady state and every line means work was done. This gives the INFO tier content it did not have.

Redis before the daemon

Both rc.d scripts start Redis in rc_pre when REDIS_HOST is local. The daemon whitelists without Redis, but nothing expires until it appears, so a boot that races Redis leaves expiry off until the next restart. A failure is not fatal, since a non-zero rc_pre would abort a start the daemon can manage, and a REDIS_HOST elsewhere is left alone.

The daemon cannot do this itself. Starting a service needs proc exec, which its pledge deliberately excludes under CTL: IOCTL, and rc_pre already runs as root.

Blocklists

update_dns_blocklist.sh fetches HaGeZi's pro and tif domain lists, the two the README already recommended, and converts them to Unbound's local-zone format. Only plausible domains are written out: the files carry a header, and the wildcard variants carry *. prefixes, either of which would take the whole include file with it when Unbound loads it. The guards against a short or collapsed list are unchanged, and a new test covers the converter.

The installer keeps an existing copy rather than replacing it, since the sources, the allowlist hook and the thresholds are all meant to be edited.

Redis timeouts

The redis crate blocks indefinitely on connect, read and write by default, so a stalled server holds a worker for good. Eight of those and the pool is gone, and the firewall stops whitelisting with nothing in the log. Connect, read and write are now bounded at five seconds, which is generous because the store runs after the acknowledgement; it exists only so a worker cannot be lost. A timeout now surfaces as the "Failed to store to Redis" the code already handled.

Each pass also asserts that every acknowledged message finished. The acknowledgement precedes the Redis and persist writes, so an ack with no matching completion is a worker stuck in a store.

Verification

123 Rust unit tests, 19 integration tests, 250 Python tests, cargo fmt --check and clippy -D warnings clean.

The OpenBSD job is green, with every new pass proving its own claim: the upgrade pass shows the installer stopping and restarting a running daemon with different pids and an untouched config, the stopped-daemon pass shows it left stopped, the Redis pass shows rc_pre starting a Redis it had stopped first, and the expiry pass captures the summary line:

pfui_ipv4_domains: 1 addresses, 1 expired, table +0/-1, file +0/-1

21 acknowledgements, 21 completions, none out of order.

@andylemin
andylemin force-pushed the installer-upgrades branch 4 times, most recently from 41650fb to caac9fc Compare September 27, 2026 08:06
Both server installers now put the service back as they found it. rcctl check
records whether it was running, the daemon is stopped before anything is
replaced, and started again only if it had been running. install(1) renames
into place, so replacing the binary under a running daemon succeeds while that
daemon keeps the old inode: an upgrade reported success and changed nothing.
The Rust installer stops only once its build has succeeded, so a failed build
leaves the old daemon serving, and a failed restart is reported as such,
because the firewall was whitelisting a moment before. A daemon the operator
had stopped stays stopped, and only then is the enable and start advice
printed. Two passes in the OpenBSD job cover both cases, with the pids proving
the restart and the config proving it was not replaced.

The sync loop logs one line per cycle that did something: the table's size,
what expired, and what was added to or removed from the table and the file.
A cycle that changed nothing says nothing, so INFO stays quiet in a steady
state and every line means work was done.

Both rc.d scripts start Redis before the daemon when REDIS_HOST is local. The
daemon whitelists without it, but nothing expires until it appears. A failure
is not fatal, since a non-zero rc_pre would abort a start the daemon can
manage, and a REDIS_HOST elsewhere is left alone. The daemon cannot do this
itself: starting a service needs proc exec, which its pledge excludes under
CTL: IOCTL.

Redis connect, read and write are bounded. The redis crate blocks indefinitely
on all three by default, so a stalled server held a worker for good, and enough
of those retire the pool and stop the firewall whitelisting with nothing in the
log. Each pass now asserts that every acknowledged message also finished, the
acknowledgement preceding the store writes, which is how an unbounded store
call shows up.

update_dns_blocklist.sh fetches HaGeZi's pro and tif domain lists, the two the
README recommends, and converts them to Unbound's local-zone format. Only
plausible domains are written: the files carry a header, and the wildcard
variants carry '*.' prefixes, either of which would take the whole include
file with it. The guards against a short or collapsed list are unchanged. The
installer keeps an existing copy rather than replacing it, since the sources,
the allowlist hook and the thresholds are meant to be edited.

Both workflows drop their pull_request trigger and gain a concurrency group.
With a PR open they ran everything twice, and a force-push left the previous
commit's jobs running; GitHub shows push-triggered runs on the PR anyway. The
OpenBSD job also times compiling and running the tests separately, because the
test crates pull in dev-dependencies the daemon build does not and one budget
for both could not tell a slow compile from a hung test.
@andylemin
andylemin merged commit 1f7990f into master Sep 27, 2026
6 of 7 checks passed
@andylemin
andylemin deleted the installer-upgrades branch September 27, 2026 08:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant