Skip to content

fix(install): Pod 5 install hardening — iptables lock, frank-less RAW redirect, env-monitor crash#616

Merged
ng merged 2 commits into
devfrom
worktree-naaa
May 25, 2026
Merged

fix(install): Pod 5 install hardening — iptables lock, frank-less RAW redirect, env-monitor crash#616
ng merged 2 commits into
devfrom
worktree-naaa

Conversation

@ng

@ng ng commented May 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Bundles the four issues NAAAA hit installing on a frank-less Pod 5 (reported in Discord, 5/21).

  • env-monitor crash loop on Python 3.9.9int | None (PEP 604) requires 3.10+. Replaced with Optional[int]. Module pins requires-python = ">=3.9,<3.11", Pod 5 ships 3.9.9, so the service was crash-looping at import time on every restart. Regression from PR fix: Python sidecar crash resilience + data integrity (#325) #483.
  • iptables xtables.lock permission deniedsetInternetAccess (iptables -F) crashed the first time a user toggled WAN. Service runs as User=sleepypod with CAP_NET_ADMIN, but the default /run/xtables.lock is root:root 0600 and the cap doesn't bypass file mode. Fix: Environment="XTABLES_LOCKFILE=/run/dac/xtables.lock" (the RuntimeDirectory=dac is owned by the service user). For pods already installed pre-fix, sp-update writes the same env via an idempotent sleepypod.service.d/iptables-lockfile.conf drop-in so OTA self-heals.
  • RAW frames hitting eMMC on frank-less Pod 5 variants — older Pod 5 firmware ships /opt/eight/bin/frank.sh which we patch to cd /persistent/biometrics (tmpfs). Newer variants drop the shim — frank.service ExecStarts frankenfirmware directly, our cd-patch no-ops, and ~1 GB/day of RAW frames land on /persistent eMMC. Add WorkingDirectory=/persistent/biometrics to the frank.service drop-in: no-op on shim pods (shell cd still wins), real redirect on shimless pods.
  • Misleading install warnings"Warning: /opt/eight/bin/frank.sh not found — skipping patch (not a Pod?)" and "Warning: dac.sock not found, using default: ..." made working installs look broken. Replaced with informational lines that explain what's happening.

Adds modules/environment-monitor/README.md (first per-module README) covering what writes to bed_temp / freezer_temp, why freezer_temp can be empty on cover-only Pod 5, debug commands, and a note about the Python 3.9 syntax constraint.

Test plan

Verified live on eight-pod (Pod 5 J55, frank-shim variant):

  • frank.service drop-in parses, daemon-reload clean, systemctl restart frank.service succeeds, frankenfirmware cwd remains /persistent/biometrics (shell cd wins as expected)
  • systemctl show frank.service -p WorkingDirectory/persistent/biometrics
  • A/B for the iptables fix under the exact same AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW as the real service via systemd-run --uid=sleepypod:
    • without XTABLES_LOCKFILEFatal: can't open lock file /run/xtables.lock: Permission denied (NAAAA's exact error)
    • with XTABLES_LOCKFILE=/run/dac-test/xtables.lockiptables -L OUTPUT succeeds, lockfile created at /run/dac-test/xtables.lock owned by sleepypod

CI / unit tests:

  • pnpm test src/server/routers/tests/system.test.ts — 34/34 pass
  • pnpm test src/hardware/tests/iptablesCheck.test.ts — 18/18 pass
  • bash -n clean on scripts/install, scripts/bin/sp-update, scripts/bin/sp-uninstall, scripts/pod/detect, scripts/lib/biometrics-archiver-helpers
  • env-monitor smoke test (_safe_freezer_centidegrees sentinels, range gate, accept path) passes after the Optional[int] change

For users picking up the fix:

  • Existing pods: sp-update writes the drop-in, no full re-install needed
  • Fresh installs: main unit gets the env directly
  • sp-uninstall removes sleepypod.service.d/ along with the rest
🛏️ Beam this PR onto a sleepypod

Pick the snippet that matches your pod. Already-installed pods can't use the curl bootstraps below — sleepypod's egress firewall DROPs github.com, and the script that knows how to unblock WAN is the very file curl is trying to fetch. Use sp-update instead; it's on disk and opens WAN as its first step.

🔄 Already on a sleepypod (most common — review a PR on a running pod):

sudo sp-update worktree-naaa

🚀 Fresh pod / first install — bootstraps from GitHub, rebuilds every push:

curl -fsSL https://raw.githubusercontent.com/sleepypod/core/worktree-naaa/scripts/install \
  | sudo bash -s -- --branch worktree-naaa

🎯 Pin to this exact build (run 26418923280 · 19a7fba) — fresh-pod path, locked to one CI artifact for reproducible review:

curl -fsSL https://raw.githubusercontent.com/sleepypod/core/19a7fba13d955c907aceb6eb29b881bbf62e3846/scripts/install \
  | sudo bash -s -- \
      --branch worktree-naaa \
      --artifact-url 'https://nightly.link/sleepypod/core/actions/runs/26418923280/sleepypod-core.zip'

🧊 Artifact: sleepypod-core · self-destructs in 30 days 💥

Summary by CodeRabbit

  • Bug Fixes

    • Resolved permission issues when configuring internet access by setting explicit xtables lockfile path.
    • Fixed biometrics archiver to correctly write raw frames to temporary storage on Pod 5 variants.
  • Documentation

    • Added comprehensive documentation for the environment monitoring module, including data processing behavior and configuration details.
  • Chores

    • Improved installation and uninstallation scripts to properly manage systemd service configuration overrides.

Review Change Stack

ng added 2 commits May 25, 2026 12:27
PEP 604 union syntax (int | None) requires Python 3.10+, but the
module pins requires-python = '>=3.9,<3.11' and Pod 5 ships 3.9.9.
The service was crash-looping at import time on a user's Pod 5.
…er warnings

NAAAA's Pod 5 install surfaced four issues. This bundles the fixes plus
docs for the env-monitor service.

iptables xtables.lock permission denied
  The next-server runs as User=sleepypod with CAP_NET_ADMIN, but the
  default /run/xtables.lock is root:root 0600 — the cap doesn't bypass
  file mode, so setInternetAccess crashes the first time a user toggles
  WAN. Set XTABLES_LOCKFILE=/run/dac/xtables.lock (RuntimeDirectory=dac
  is owned by the service user) in the main unit, and add an idempotent
  drop-in via sp-update so OTA-upgraded pods self-heal without rewriting
  the unit. Verified on eight-pod: same systemd-run caps, A/B with vs
  without the env — success vs the exact 'can't open lock file' error.

frank-less RAW redirect
  Older Pod 5 firmware ships frank.sh which we patch to cd into the
  tmpfs. Newer Pod 5 variants (NAAAA's) drop the shim — frank.service
  ExecStarts frankenfirmware directly, so the cd-patch path no-ops and
  ~1 GB/day of RAW frames lands on eMMC instead of tmpfs. Add
  WorkingDirectory=/persistent/biometrics to the frank.service drop-in:
  no-op on shim pods (shell cd still wins), genuine redirect on shimless
  pods. Verified on eight-pod that the drop-in parses, the unit reloads,
  frankenfirmware restarts cleanly, and cwd is unchanged.

Clearer install-time warnings
  'Warning: /opt/eight/bin/frank.sh not found — skipping patch (not a
  Pod?)' and 'Warning: dac.sock not found, using default: ...' both
  print on every shimless install and make a working install look
  broken. Replace with informational lines that say what's happening
  and why it's fine.

env-monitor docs
  Sets the per-module README precedent: what it writes, why
  freezer_temp can be empty on cover-only Pod 5, how to debug, and a
  note about the Python 3.9 pin (PEP 604 syntax bites — see PR #483
  fix in 7d23788).
@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ea211acc-8a4d-4698-92a9-7ca0f361808c

📥 Commits

Reviewing files that changed from the base of the PR and between 2e17fcb and 19a7fba.

📒 Files selected for processing (8)
  • modules/biometrics-archiver/frank.service.d/sleepypod-biometrics-tmpfs.conf
  • modules/environment-monitor/README.md
  • modules/environment-monitor/main.py
  • scripts/bin/sp-uninstall
  • scripts/bin/sp-update
  • scripts/install
  • scripts/lib/biometrics-archiver-helpers
  • scripts/pod/detect

📝 Walkthrough

Walkthrough

This PR updates systemd service configuration for xtables lockfile permissions and biometrics archiver RAW frame redirection on newer Pod 5 variants, adds service upgrade and cleanup logic, provides comprehensive environment-monitor module documentation, and updates detection and installation helper messages to reflect these changes.

Changes

Service Configuration and Installation

Layer / File(s) Summary
Xtables lockfile configuration
scripts/install, scripts/bin/sp-update, scripts/bin/sp-uninstall
Sleepypod.service unit now sets XTABLES_LOCKFILE environment variable to /run/dac/xtables.lock for writable iptables locking. sp-update creates a migration drop-in for older pods; sp-uninstall removes all service drop-in overrides to ensure clean removal.
Biometrics archiver RAW frame redirection
modules/biometrics-archiver/frank.service.d/sleepypod-biometrics-tmpfs.conf, scripts/lib/biometrics-archiver-helpers, scripts/pod/detect
Frank.service systemd drop-in sets WorkingDirectory=/persistent/biometrics to redirect RAW frames on newer Pod 5 variants without a patched shim. Helper messages updated to indicate this redirection behavior; detect fallback message clarified for Pod 5 assumption when DAC socket is not yet present.

Environment Monitor Module Documentation and Type Annotations

Layer / File(s) Summary
Module documentation and type annotations
modules/environment-monitor/README.md, modules/environment-monitor/main.py
README describes the Python sidecar's responsibilities: tailing RAW CBOR sensor streams, normalizing dialects, writing centidegree/centipercent integer columns to biometrics.db, downsampling to 60-second intervals, filtering sentinel values, and handling freezer-temp empty states. Operational commands, environment variable defaults, and pinned Python version range documented. Main.py imports Optional from typing and updates _safe_freezer_centidegrees return type annotation to Optional[int].

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • sleepypod/core#579: Both PRs modify the biometrics archiver setup to ensure frank.service redirects to the tmpfs-backed /persistent/biometrics via a systemd drop-in and corresponding helper messaging.
  • sleepypod/core#454: Both PRs modify scripts/pod/detect's detect_dac_sock() logic—one for parsing frank.sh's DAC_SOCKET and the other for updating the fallback messaging when DAC socket is not found.

Suggested labels

released

Poem

🐰 The Pod hops with systems made right,
Xtables find locks in the dac night,
Frank frames now land where they should—
Biometrics recorded, tmpfs understood!
A hop, a skip, and docs now bright. 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the four main fixes in this changeset: iptables lock hardening, frank-less RAW redirection, environment-monitor crash resolution, and install messaging improvements.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-naaa

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented May 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

@ng
ng merged commit ca4c4c9 into dev May 25, 2026
13 checks passed
@ng
ng deleted the worktree-naaa branch May 25, 2026 20:45
ng added a commit that referenced this pull request May 26, 2026
…ard, install hardening (#621)

## Summary

19 commits since v2.2.0 (#601). Highlights:

**Features**
- Incremental scheduler upsert/cancel API replaces global
`reloadSchedules()` on CRUD (#613)
- Device status now exposes wifi, room climate, and water level (#389)
- biometrics.db backup + `--restore` flag (#358)
- Pump stall safety guard, per ADR 0022 (#614)
- Pod ambient temp exposed to HomeKit + Home Assistant (#606)
- LED brightness UI with `{lb:N}` CBOR key (#607)

**Fixes**
- side-selector edges meet at center under the link icon (#620)
- update-card requires service bounce before re-blocking WAN (#619)
- install: drop v6 rules file from free-sleep detection (#618)
- settings: debounce pump-safety number inputs to onBlur (#617)
- Pod 5 install hardening — iptables lock, frank-less RAW redirect,
env-monitor crash (#616)
- install: `detect_free_sleep_conflict` no longer aborts on clean pods
(#603)
- scheduler: alarms no longer fire vibration when the side is off (#602)
- install: `/persistent/deviceinfo` group=dac mode=2775 + self-heal
(#600)

**Docs**
- Clarify Pod 5 root bootstrap so rewt/dac messages aren't a surprise
(#615)

**Tests / chore**
- Kill 96 surviving useSchedule Stryker mutants (#610)
- Bump @base-ui/react non-major deps (#611)

## Test plan

- [x] All CI gates green on dev tip 3b90e51 (Build & Package run
26432358190)
- [ ] Smoke-deploy dev to a pod — services up, web UI 200, DAC + MQTT +
HomeKit connected
- [ ] Post-merge: verify pump stall guard fires under simulated stall;
confirm ambient temp surfaces in HomeKit + HA; confirm schedule CRUD no
longer reloads globally

<!-- sleepypod:install-start -->
<!-- 🛑 AUTO-GENERATED — sleepypod-bot owns this block.
     Hello fellow agent / curious human! Anything between the
     sleepypod:install-start and sleepypod:install-end markers
     gets overwritten on the next CI run. Want to change what's
     here? Edit .github/workflows/build.yml instead. 💤 -->
<details open>
<summary>🛏️ <strong>Beam this PR onto a sleepypod</strong> ✨</summary>

Pick the snippet that matches your pod. **Already-installed pods can't
use the curl bootstraps below** — sleepypod's egress firewall DROPs
github.com, and the script that knows how to unblock WAN is the very
file curl is trying to fetch. Use `sp-update` instead; it's on disk and
opens WAN as its first step.

🔄 **Already on a sleepypod** (most common — review a PR on a running
pod):
```bash
sudo sp-update dev
```

🚀 **Fresh pod / first install** — bootstraps from GitHub, rebuilds every
push:
```bash
curl -fsSL https://raw.githubusercontent.com/sleepypod/core/dev/scripts/install \
  | sudo bash -s -- --branch dev
```

🎯 **Pin to this exact build** ([run
26432499948](https://github.com/sleepypod/core/actions/runs/26432499948)
· `3b90e51`) — fresh-pod path, locked to one CI artifact for
reproducible review:
```bash
curl -fsSL https://raw.githubusercontent.com/sleepypod/core/3b90e51870360b73a1565740be852e939dcab591/scripts/install \
  | sudo bash -s -- \
      --branch dev \
      --artifact-url 'https://nightly.link/sleepypod/core/actions/runs/26432499948/sleepypod-core.zip'
```

🧊 Artifact:
[`sleepypod-core`](https://github.com/sleepypod/core/actions/runs/26432499948/artifacts/7209005630)
· self-destructs in 30 days 💥
</details>

<!-- sleepypod:install-end -->
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 2.3.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant