Repository navigation
WolfStack v22.10.4
v22.10.4: IP-mapping reconciliation loop — silent startup failures recover automatically (PapaSchlumpf)
Real bug: apply_ip_mappings() was called exactly once at startup
from std::thread::spawn. Each enabled mapping calls
apply_mapping_rules(), which calls detect_wolfnet_gateway_ip() —
and fails outright if wolfnet0 isn't up yet. The error is logged
at warn level and never retried. Right after a reboot, when WolfStack
starts before WireGuard finishes establishing the mesh and assigns
the wolfnet0 address, every DNAT rule gets dropped on the floor and
the operator only sees one warn line in journalctl that nobody reads.
PapaSchlumpf's symptom — Frigate (10.10.10.2:5000 → 10.0.10.4:5000)
and Home Assistant VM with a WolfNet IP both unreachable on a fresh
node — maps cleanly onto this. Same node configuration that works at
home, broken here, with no obvious error: classic "race lost at boot,
no recovery path".
Fix: add a tokio reconciliation loop in main.rs that re-runs
apply_ip_mappings() periodically — every 5s while at least one
mapping is still failing, every 30s once everything's settled. The
function is idempotent (apply_mapping_rules calls
purge_mapping_rules first), so re-running over a healthy state is
a safe ~2 iptables ops per enabled mapping per tick.
Also tightened apply_ip_mappings():
- Returns the count of failed mappings (so the loop can decide its
cadence — fast retry while broken, slow retry once healthy). - Skips disabled mappings via an explicit
continueinstead of
relying onapply_mapping_rules's internal short-circuit. Same
behavior, clearer intent, matches the new "count of failed
enabled mappings" return contract. - Removed the empty
if count > 0 {}block that was leftover from
a removed log line.
Race notes: reconcile can fire concurrently with API CRUD operations
on mappings. Both call apply_mapping_rules → purge_mapping_rules
- add. Worst case is brief overlap that ends up applying the same
rule twice (idempotent — first add wins, second purges-and-re-adds,
both leave the same final state). Eventually consistent without a
mutex, which keeps the API hot path fast.
Tests: full suite 671/671 (no test changes — behavior change is in
a startup loop that requires a running daemon to verify). The unit
tests for parse_port_mappings (v22.10.3) all still pass.
Untested live: a node that boots with WolfStack starting before
wolfnet0 — the exact scenario that motivates the loop. Should
converge within 5–35s of wolfnet0 coming up. PapaSchlumpf's box is
the canonical reproducer; we'll know it works when Frigate and HASS
are reachable on their mapped IPs after a reboot without him having
to toggle each mapping in the UI.
Verifying this release
Each binary is signed via cosign keyless OIDC (no key distribution — signing identity is the GitHub Actions workflow itself, anchored to the Sigstore Fulcio CA and the Rekor transparency log) and ships with a SLSA build provenance attestation.
Verify the cosign signature:
cosign verify-blob \
--bundle wolfstack-x86_64.cosign.bundle \
--certificate-identity-regexp 'https://github.com/wolfsoftwaresystemsltd/WolfStack/\.github/workflows/release\.yml@.*' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
wolfstack-x86_64
Verify the build provenance:
gh attestation verify wolfstack-x86_64 --repo wolfsoftwaresystemsltd/WolfStack
Verify the SHA-256 checksum:
sha256sum -c SHA256SUMS
Artifacts
wolfstack-x86_64/wolfstack-aarch64— static musl binaries (Linux x86_64 and ARM64 / Raspberry Pi 4+).wolfstack-<arch>.cosign.bundle— cosign signature bundle (cert + signature + Rekor entry).SHA256SUMS— checksums for both binaries.
For per-version history see CHANGELOG.md.