Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Auto-Update

Auto-update shipped across Phases 8c–8g: update-check + verified download as CLI subcommands (8c Foundation), then atomic swap + health check + rollback wired into the daemon itself (8d Wiring onward). All of it is in the current release; the sections below cover the CLI entry points and the daemon flow.

CLI update commands (Phase 8c)

Two CLI commands, manually triggered by the operator:

dvb-warppool-cli check-update

Fetches the latest release from the project forge (Forgejo) and compares it against the installed version (CARGO_PKG_VERSION from the CLI binary).

$ dvb-warppool-cli check-update
Installed:    1.4.0
Latest:       v1.4.1 (Release v1.4.1)
Update:       1.4.0 → 1.4.1  (available)

Release-Assets:
  · dvb-warppool-v1.4.1-x86_64-unknown-linux-gnu.tar.gz (15728640 bytes)
  · dvb-warppool-v1.4.1-aarch64-unknown-linux-gnu.tar.gz (14680064 bytes)
  · dvb-warppool_1.4.1-1_amd64.deb (9437184 bytes)
  · SHA256SUMS (1280 bytes)
  · SHA256SUMS.bundle (2048 bytes)

Optional --json for scripting. --repo owner/name for forks or tests.

dvb-warppool-cli download-update

Downloads a release asset with sha256 verification. The Linux/macOS assets are tarballs (dvb-warppool-<tag>-<triple>.tar.gz); the CLI verifies the tarball’s sha256 and then extracts the dvb-warppool-daemon binary to --to. It performs no atomic swap and no restart — that’s the operator’s job, so the workflow stays auditable.

# Host-matching asset, auto-detected via the Rust target-triple
# (x86_64-unknown-linux-gnu / aarch64-unknown-linux-gnu / *-apple-darwin)
$ dvb-warppool-cli download-update \
    --to /tmp/dvb-warppool-daemon \
    --sha256 abcd1234...  # tarball-sha aus SHA256SUMS

Download: dvb-warppool-v1.4.1-x86_64-unknown-linux-gnu.tar.gz (15728640 bytes)
✓ 15728640 bytes geladen, sha256=abcd1234...
✓ dvb-warppool-daemon (13007200 bytes) aus dem Tarball extrahiert → /tmp/dvb-warppool-daemon
→ Installieren (Operator-Schritt — kein Auto-Swap):
    sudo install -m 755 /tmp/dvb-warppool-daemon /usr/local/bin/dvb-warppool-daemon
    sudo systemctl restart dvb-warppool

The sha256 you pass is the tarball’s hash — the SHA256SUMS lines cover the published assets (the .tar.gz), not the binary inside. On mismatch the downloaded tarball is deleted — no “tainted download” left lying around. --asset <substring> overrides the host auto-detection. --tag vX.Y.Z downloads a specific release instead of latest — to pin a known-good version or downgrade off a bad one (pass the tag with its v prefix; it hits releases/tags/<tag> instead of releases/latest).

#!/bin/bash
set -euo pipefail
TAG=$(dvb-warppool-cli check-update --json | jq -r .latest)
[ "$TAG" = "null" ] && exit 0  # no update

# fetch SHA256SUMS from the release and verify with cosign
curl -sSfLO https://git.warppool.org/dvb-projekt/dvb-WarpPool/releases/download/$TAG/SHA256SUMS
curl -sSfLO https://git.warppool.org/dvb-projekt/dvb-WarpPool/releases/download/$TAG/SHA256SUMS.bundle
curl -sSfLO https://git.warppool.org/dvb-projekt/dvb-WarpPool/raw/branch/main/cosign.pub
# Project-key signing since the forge migration: the signature ships as a
# self-contained cosign 3.x *bundle*, so verify with --bundle, not --signature.
# GitHub-era releases (<= v1.15.0) were keyless-signed with a detached
# SHA256SUMS.sig — those assets are gone with the account.
cosign verify-blob \
  --key cosign.pub \
  --bundle SHA256SUMS.bundle SHA256SUMS

# extract the expected sha256 for the host tarball
TRIPLE="x86_64-unknown-linux-gnu"   # or aarch64-unknown-linux-gnu / *-apple-darwin
ASSET="dvb-warppool-${TAG}-${TRIPLE}.tar.gz"
EXPECTED=$(awk -v a="$ASSET" '$2 == a {print $1}' SHA256SUMS)

# download + verify the tarball, then extract the daemon binary to --to
dvb-warppool-cli download-update --asset "$TRIPLE" \
  --to /tmp/dvb-warppool-daemon --sha256 "$EXPECTED"

# manual swap (the daemon's POST /api/admin/update automates this)
sudo install -m 755 /tmp/dvb-warppool-daemon /usr/local/bin/dvb-warppool-daemon
sudo systemctl restart dvb-warppool

Crate internals (warppool-autoupdate)

#![allow(unused)]
fn main() {
use warppool_autoupdate::UpdateChecker;

let checker = UpdateChecker::new("dvb-projekt/dvb-WarpPool");
let release = checker.fetch_latest().await?;
if let Some(newer) = checker.is_newer_than(&release, env!("CARGO_PKG_VERSION"))? {
    // update available
}
}

Modules:

  • version — semver-subset parser (major.minor.patch + optional -pre), compared via Ord. Pre-release < stable (semver convention).
  • release — forge release API (GitHub-compatible JSON schema) with minimal serde deserialization (no octocrab — that would be 30+ transitive deps for 3 fields).
  • downloaddownload_verified(client, url, dest, expected_sha) with streaming write + SHA-256 accumulator. Mismatch → file deleted.
  • swapatomic_swap(new, current, backup_to) with POSIX rename (atomic on the same filesystem). Sets chmod 0755 on the new binary, optionally backs up the old one to backup_to.

Phase 8d — API endpoints

Since Phase 8d the daemon exposes two admin-protected endpoints for GUI-driven auto-update (activated via env-var WARPPOOL_AUTOUPDATE_REPO=owner/name, otherwise 503).

GET /api/admin/update-check

Returns latest-release metadata + newer flag in a single request.

curl -H "Authorization: Bearer $TOKEN" \
     http://localhost:18334/api/admin/update-check
{
  "current": "1.4.0",
  "latest": "v1.4.1",
  "name": "Release v1.4.1",
  "prerelease": false,
  "newer": "1.4.1",
  "assets": [
    {"name": "dvb-warppool-v1.4.1-x86_64-unknown-linux-gnu.tar.gz", "size": 15728640, "url": "..."},
    {"name": "SHA256SUMS", "size": 1280, "url": "..."}
  ]
}

Audit: update.check with the resolved latest tag as target.

POST /api/admin/update

Downloads the chosen asset, verifies sha256, and — for the Linux/macOS .tar.gz assets — extracts the dvb-warppool-daemon binary before the atomic rename into target_path. asset is an optional substring match; omit it and the daemon picks the asset matching its own host target-triple. Native installers (.msi/.dmg/.deb/.rpm/.AppImage) are rejected here — use the platform installer. Does not send a restart — the operator does that via systemctl restart from the restart_hint field.

curl -X POST -H "Authorization: Bearer $TOKEN" \
     -H "Content-Type: application/json" \
     -d '{
       "asset": "x86_64-unknown-linux-gnu",
       "sha256": "abcd1234...",
       "target_path": "/usr/local/bin/dvb-warppool-daemon",
       "backup_path": "/var/lib/dvb-warppool/backup/daemon.v1.4.0"
     }' \
     http://localhost:18334/api/admin/update

{
  "tag": "v1.4.1",
  "asset": "dvb-warppool-v1.4.1-x86_64-unknown-linux-gnu.tar.gz",
  "bytes": 15728640,
  "sha256": "abcd1234...",
  "target_path": "/usr/local/bin/dvb-warppool-daemon",
  "backup_path": "/var/lib/dvb-warppool/backup/daemon.v1.4.0",
  "installed_from_archive": true,
  "restart_hint": "systemctl restart dvb-warppool"
}

Audit: update.applied (ok=true) or update.failed (ok=false with details). The operator can review the update history via /api/admin/audit.

Activation

In dvb-warppool.service (systemd):

[Service]
Environment="WARPPOOL_AUTOUPDATE_REPO=dvb-projekt/dvb-WarpPool"

Or directly:

WARPPOOL_AUTOUPDATE_REPO=dvb-projekt/dvb-WarpPool dvb-warppool-daemon

Phase 8e — Periodic auto-check + SSE event

When auto-update is enabled (WARPPOOL_AUTOUPDATE_REPO=…), the daemon spawns a background task that runs fetch_latest every N hours. On a newer release an update_available SSE event is pushed to the event bus — the UI subscribes to it and can show an “Update available” banner without the operator having to click update-check manually.

Activation

# Standard: 24h interval
WARPPOOL_AUTOUPDATE_REPO=dvb-projekt/dvb-WarpPool dvb-warppool-daemon

# Custom interval
WARPPOOL_AUTOUPDATE_REPO=dvb-projekt/dvb-WarpPool \
WARPPOOL_AUTOUPDATE_INTERVAL_HOURS=6 \
  dvb-warppool-daemon

# Periodic disabled, on-demand only via /api/admin/update-check
WARPPOOL_AUTOUPDATE_REPO=dvb-projekt/dvb-WarpPool \
WARPPOOL_AUTOUPDATE_INTERVAL_HOURS=0 \
  dvb-warppool-daemon

Initial delay is 60s after daemon start so that not every pool instance hits the forge at the same time.

SSE event schema

Subscribe via Browser EventSource at /api/events. The event looks like:

event: update_available
data: {"type":"update_available","at":"2026-05-26T12:00:00Z","current":"0.1.0","latest":"v0.2.0","name":"Release 0.2.0","prerelease":false}

JavaScript example for a UI banner:

const es = new EventSource("/api/events");
es.addEventListener("update_available", (ev) => {
  const data = JSON.parse(ev.data);
  showBanner(`Update available: ${data.current} → ${data.latest}`);
});

On errors (rate-limit, network-down)

The task logs warn! and retries on the next interval. With the default 24 h interval a pool makes fewer than 2 release-API requests a day, so no rate limit on the forge side is anywhere near being a concern.

Phase 8f — Systemd OnFailure rollback

So that a bad auto-update doesn’t leave the daemon in a permanently-down state, the shipped dvb-warppool.service has an OnFailure= hook that on repeated crashes (StartLimitBurst=4 times within StartLimitInterval=300s) restores the most recent backup and restarts.

Layout

/usr/lib/systemd/system/dvb-warppool.service           ← main service
/usr/lib/systemd/system/dvb-warppool-rollback.service  ← oneshot helper
/usr/lib/dvb-warppool/rollback.sh                      ← restore script

Flow

  1. Daemon starts with the new binary.
  2. Crash within 5s → systemd Restart=on-failure retries.
  3. After 4 crashes in 5 minutes → systemd gives up and triggers OnFailure=dvb-warppool-rollback.service.
  4. rollback.sh:
    • Finds /var/lib/dvb-warppool/backup/daemon.* (youngest mtime wins)
    • Atomically installs it to /usr/bin/dvb-warppool-daemon (with install -m 755)
    • Moves the backup to <backup>.applied-<timestamp> so it isn’t picked again as a rollback source (prevents restart loops)
    • systemctl restart --no-block dvb-warppool.service
  5. Daemon now starts with the old binary — back online.
  6. Operator sees in the journal: journalctl -u dvb-warppool-rollback.service

Configuration via env-vars

In /etc/default/dvb-warppool (or systemctl edit dvb-warppool-rollback):

WARPPOOL_BACKUP_DIR=/var/lib/dvb-warppool/backup
WARPPOOL_TARGET_BIN=/usr/bin/dvb-warppool-daemon
WARPPOOL_SERVICE=dvb-warppool.service

All have sensible defaults — only override if the deployment deviates from the standard.

Creating a backup

Rollback only works if a backup was saved before the update. So set backup_path in the POST /api/admin/update body:

{
  "asset": "x86_64-unknown-linux-gnu",
  "sha256": "...",
  "target_path": "/usr/bin/dvb-warppool-daemon",
  "backup_path": "/var/lib/dvb-warppool/backup/daemon.v1.4.0"
}

The atomic_swap function moves the current binary to backup_path before installing the new one — so on a crash rollback.sh has something to restore.

Testing the helper (local, without systemd)

tmp=$(mktemp -d)
bin=$(mktemp -d)
echo "OLD" > "$tmp/daemon.v0.1.0"
echo "CRASHY" > "$bin/dvb-warppool-daemon"
# mock systemctl so no real restart runs
printf '#!/bin/bash\necho mocked\n' > "$tmp/systemctl"
chmod +x "$tmp/systemctl"
PATH="$tmp:$PATH" \
  WARPPOOL_BACKUP_DIR="$tmp" \
  WARPPOOL_TARGET_BIN="$bin/dvb-warppool-daemon" \
  packaging/systemd/rollback.sh
cat "$bin/dvb-warppool-daemon"  # → OLD

Phase 8g — Cosign-verify integrated

POST /api/admin/update can optionally run a cosign verify-blob subprocess invocation before the sha256 check. GitHub-era releases (≤ v1.15.0) used sigstore’s keyless OIDC flow; since the move to the self-hosted forge, verification uses the project cosign key (cosign.pub in the repo root).

Why subprocess instead of pure-rust sigstore

The pure-rust sigstore crate would pull in 30+ transitive deps (ASN.1, X.509, certificate-chain parser, rekor client). A cosign CLI subprocess is:

  • Transparent — the operator sees the exact command in the audit log
  • Current — cosign updates come from sigstore, not from us
  • Minimal — no extra Rust deps

Trade-off: the operator must have cosign installed.

Activation

Two prerequisites:

  1. Server-side env-var WARPPOOL_COSIGN_BIN=/usr/local/bin/cosign set (typically in the systemd unit’s Environment="...").
  2. Request body cosign_verify: true + cosign_args: [...].

If cosign_verify=true but the env-var is not set → 500 “WARPPOOL_COSIGN_BIN env-var not set”. Deliberately hard-fail so the operator doesn’t get a false sense of security.

Example request

curl -X POST -H "Authorization: Bearer $TOKEN" \
     -H "Content-Type: application/json" \
     -d '{
       "asset": "x86_64-unknown-linux-gnu",
       "sha256": "abcd1234...",
       "target_path": "/usr/bin/dvb-warppool-daemon",
       "backup_path": "/var/lib/dvb-warppool/backup/daemon.v1.4.0",
       "cosign_verify": true,
       "cosign_args": [
         "--bundle=/etc/dvb-warppool/SHA256SUMS.bundle",
         "--key=/etc/dvb-warppool/cosign.pub"
       ]
     }' \
     http://localhost:18334/api/admin/update

Internally (after the asset download) the server fetches the release’s signed SHA256SUMS asset and runs cosign verify-blob on that file — the release signs only SHA256SUMS, not each individual asset, so SHA256SUMS is the blob the signature covers. cosign verify-blob checks the signature against its last (blob) argument, so that argument must be SHA256SUMS, never the asset. Once the signature checks out the server binds the asset to it: the downloaded asset’s sha256 (the sha256 you passed) must appear as that asset’s line in the now signature-verified SHA256SUMS, otherwise the update is refused (403):

# the blob (last arg) is the SHA256SUMS file the release signed —
# NOT the asset; per-asset integrity comes from its line inside it.
$WARPPOOL_COSIGN_BIN verify-blob \
    --bundle=<path to SHA256SUMS.bundle> \
    --key=<path to cosign.pub> \
    <downloaded SHA256SUMS>

Note that cosign_args are passed through verbatim, so the bundle (or, for an archived GitHub-era release, a detached --signature) has to be reachable from the daemon — a local path or a URL cosign can fetch.

If cosign exits ≠ 0 → the downloaded asset (and the SHA256SUMS temp) are deleted, response 403, audit update.failed with cosign verify-blob failed: exit N. The same 403 fires if the asset’s sha256 is not listed in — or mismatches — the verified SHA256SUMS.

Security model

LayerWhat is checked
sha256File integrity against the hash supplied by the operator
cosign verify-blobSignature authenticity against the project cosign key (cosign.pub)
cosign_argsFully operator-controlled — the server only appends the downloaded SHA256SUMS file as the last (blob) arg
Audit trailupdate.applied / update.failed with details

Defense in depth: sha256 alone doesn’t protect against a “malicious operator with the correct hash”; cosign alone doesn’t protect against bit-drift during download. Both together are the right production configuration.

Phase 8 — completed

SubWhat
8amdBook Documentation Site
8bReproducible Builds (lto=fat + –remap-path-prefix + repro-CI)
8cAuto-Update Foundation Crate + CLI
8dAuto-Update API (GET update-check + POST update)
8ePeriodic Auto-Check + UpdateAvailable SSE event
8fSystemd OnFailure-Rollback (StartLimitBurst + Hook + rollback.sh)
8gCosign-Verify integrated into POST /api/admin/update

Platform availability

Since the move to the self-hosted forge, CI runs on the project’s own runners — and there is currently neither a macOS nor a Windows runner. That changes what a release contains:

PlatformAssetBuilt by
Linux x86_64 / aarch64.tar.gz, .deb, .rpm, .AppImageCI, every release
Docker (amd64 + arm64)image on git.warppool.orgCI, every release
macOS Intel / Apple Silicon.tar.gz, .dmgbuilt manually on a Mac and attached to the release afterwards
Windows x86_64.msipaused — no Windows runner

Consequences for auto-update:

  • macOS assets can lag the release. They are attached shortly after publish, not at publish time, so a macOS host that checks for an update within that window sees the new tag but not yet its asset. The binaries job deliberately does not depend on the macOS build, so a Mac being offline never blocks a release. Because the manual step re-signs the checksums, SHA256SUMS and SHA256SUMS.bundle are replaced when the macOS assets land — re-fetch both if you grabbed them before.
  • Windows has no asset at all right now. The CLI’s x86_64.msi host mapping and the WiX packaging sources (packaging/windows/, cargo-wix) are kept maintained, and the .msi returns as soon as a Windows runner is registered. Until then, Windows users build from source.
  • macOS GUI installs go through the .dmg 1-click flow (POST /api/admin/update/install-macos-dmg) once that asset is present.

Other limitations today

  • No diff updates (full binary only). At 15 MB / release that’s harmless.
  • Cosign-verify is operator-side (see flow above), not built into the CLI — deliberately, so the operator sees the trust anchor.

See also