Commit Graph

22 Commits

Author SHA1 Message Date
db0ebe1675 fix(meshtastic): ExecStartPre clears stale socket so the daemon survives restart (ref #897)
Some checks are pending
License Headers / check (push) Waiting to run
uvicorn/uvloop refuses to rebind an existing UDS ('Address already in use') →
crash-loop on restart. ExecStartPre=+/bin/rm -f the socket (root, clears any
owner in the sticky /run/secubox) before ExecStart.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-22 10:46:56 +02:00
536bcd1c0f fix(meshtastic): postinst must NOT chown shared parents (broke auth OTP + exposed secrets) (ref #897)
install -d -o root -g root chowned /etc/secubox to root:root (auth/users engine
= secubox lost write → OTP login 500/JSON.parse error) and set
/etc/secubox/secrets to 0755 (secrets exposure). chmod-only on existing shared
parents, never chown; secrets stays 0700.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-22 10:43:22 +02:00
66366537df fix(meshtastic): daemon survives an unreachable MQTT broker at startup (ref #897)
Deploy found a crash-loop: the shipped default config has [shared_grid] set, but
the opt-in mosquitto is not running, so Bridge.start()/the paho adapter's
synchronous connect() raised ConnectionRefusedError and killed the daemon.
Bridge.start() now skips an unreachable broker (log + continue); the paho adapter
uses connect_async + loop_start (non-blocking, auto-reconnect). +regression test.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-22 10:36:42 +02:00
dc145db9c3 fix(meshtastic): final review — panel webui→ctl wiring (NNP+sudoers user) + mosquitto Recommends + minors (ref #897)
Reconcile the three pieces the webui→ctl delegation needs to actually work:
NoNewPrivileges=yes on the daemon unit neutralized sudo, and sudoers.d
granted to the wrong user (secubox instead of secubox-meshtastic, the
daemon's actual User=). Verified _ctl_cb's systemd-run argv matches the
sudoers command lines for set-mode/set-grid token-for-token.

Also demote mosquitto from Depends to Recommends (module never installs/
runs it; README already documents opt-in setup) and add sudo to Depends
for the ctl delegation. Minor cleanups: unused `field` import in model.py,
dead gridpolicy re-export in daemon.py, and a malformed broker port
(e.g. host:abc) no longer crashes Bridge.start() at daemon startup.

Deferred: append a note to the multigrid design spec about the
gridpolicy/bridge default broker-port mismatch (8883 vs 1883) and the
nft hostname-resolved-once caveat for future on-grid containment work.
2026-07-22 09:02:55 +02:00
72187424d4 feat(meshtastic): debian packaging + README (ref #897)
Finalizes the secubox-meshtastic package for install: debian/install
(api -> usr/lib/secubox/meshtastic/api, sbin -> usr/sbin, www, nginx
route, menu entry, config example, mosquitto broker reference conf),
debian/postinst (secubox-meshtastic system user in dialout, restricted
leaf dirs under 0755 shared parents per #511/#474, config seed-if-absent,
enable+start the daemon which runs fine with radio: absent), debian/prerm,
debian/rules (0440 sudoers drop-in + dh_installsystemd --name=), and
debian/control deps (fastapi/uvicorn/pydantic/paho-mqtt/mosquitto,
secubox-core). Adds README documenting the API, the three grids, passive
mode, and the meshtastic pip (non-Debian) dependency note.

Clean dpkg-buildpackage build verified (secubox-meshtastic_0.1.0-1~bookworm1_all.deb),
60/60 tests pass, SPDX headers clean.
2026-07-22 08:50:56 +02:00
340bb6a650 feat(meshtastic): systemd + nginx + menu + private broker (ref #897)
- systemd/secubox-meshtasticd.service (+ debian/ copy): dedicated
  secubox-meshtastic user/group, dialout for serial, ProtectSystem=strict
  hardening, DeviceAllow for ttyUSB/ttyACM. No RuntimeDirectory=secubox
  (ref #896 landmine) — daemon binds its socket directly into the shared
  1777 /run/secubox.
- nginx/meshtastic.conf: static panel + dedicated-socket API proxy,
  mirrors secubox-profiles' prefix-preserving proxy_pass convention.
- menu.d/71-meshtastic.json: panel entry under the mesh category.
- conf/mosquitto-secubox-meshtastic.conf: private shared_grid broker
  listener bound to the MirrorNet IP (10.10.0.1), anonymous access
  disabled, creds/ACL left to the operator.
2026-07-22 08:40:31 +02:00
6d5139ec7b feat(meshtastic): webui panel (ref #897)
Add www/meshtastic/index.html — cyan hybrid-dark panel (Courier Prime w/
monospace fallback, no external CDN/fonts/tiles) with 5 tabs: Nodes (list +
offline canvas map plotting positioned nodes, ringed/colored by SNR), Messages
(per-channel view + send via POST /send), Channels (per-channel grid
off/shared/on toggle via POST /grid), Sniffer (census table + channel-activity
bars from /packets), and Grid (radio present/absent banner + mode selector via
POST /mode). Reads localStorage.sbx_token, shows a persistent auth-required
banner on 401, and a radio-absent banner throughout.
2026-07-22 08:32:35 +02:00
760c28eab5 fix(meshtastic): wire daemon main() to web.create_app(cache,send_cb,ctl_cb) (ref #897) 2026-07-22 08:25:37 +02:00
c9ab0fa4ad feat(meshtastic): FastAPI webui backend (ref #897)
create_app(cache, send_cb, ctl_cb) exposes GET status/nodes/messages/packets
over the injected StateCache, and POST send/mode/grid — mode and grid are
validated against config.MODES/GRIDS (422) BEFORE delegating to ctl_cb,
mirroring secubox-profiles' validate-before-delegate pattern. All routes
require the real secubox_core.auth.require_jwt.
2026-07-22 08:17:45 +02:00
16fb771d7e fix(meshtastic): task-9 review — egress drop-in to /etc/nftables.d + accurate comments (ref #897)
Fixes:
1. Change EGRESS_DROPIN path to /etc/nftables.d/ (matches actual repo structure)
2. Update docstring + _render_egress comments: soften overstate default-drop claim
   - These are allow-list add-ons (policy accept), not default-drop enforcers
   - Fail-safe: empty ruleset never OPENS egress without enabled broker
3. Fix sudoers comment: grid has manual per-token validation, not choices=
4. Add control-char escaping to _toml_str (\n, \r, \t) for valid TOML output

All existing tests still pass (14/14). visudo validation: réussi.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 08:13:52 +02:00
75c94abcd2 feat(meshtastic): secubox-meshtasticctl + sudoers (ref #897)
Root-only webui->ctl CLI for /etc/secubox/meshtastic.toml, mirroring
secubox-profiles' cli.py/actuate_paths.py conventions: set-mode/set-region/
set-role/set-grid/set-psk edit the config via a small TOML dump/validate/
atomic-swap (double-buffer/4R), apply-egress renders gridpolicy's nft rules
into an nftables drop-in (empty rule list keeps DEFAULT DROP). Each verb
requires root and appends a JSON audit line. Scoped systemd-run-wrapped
sudoers grants let the panel (User=secubox) invoke each verb without a
broader sudo surface.
2026-07-22 08:02:29 +02:00
c72129bd69 fix(meshtastic): _SerialRadio.on dispatches per-topic payload (ref #897)
Carried fix from Task 4 review: the single lambda used for all three pubsub
topics forwarded `packet` for every event, but meshtastic publishes `node=`
on meshtastic.node.updated (no `packet` kwarg) and neither on
meshtastic.connection.established. Deliver the correct kwarg per topic
(packet/node/interface). Untested (no real device), no new test required
per task brief.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 07:51:51 +02:00
9d8bf30efc feat(meshtastic): daemon engine + launcher (ref #897)
Implement Task 8: Engine class wiring radio → state → cache/passive/bridge
on packet receive, plus main() building real objects (config.load, open_serial,
paho-mqtt-backed Bridge factory imported lazily, PassiveCapture, StateCache).
Radio-absent and api.web-absent (Task 10 not yet done) paths degrade gracefully
instead of crashing. sbin/secubox-meshtasticd launches `python3 -m api.daemon`.
Tests drive Engine with MockRadio + FakeMqtt, no real device/broker. 33/33 tests
pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 07:51:45 +02:00
c2acedc80d feat(meshtastic): host-side MQTT bridge
Implement Bridge class for serial↔MQTT bridging with grid-policy-driven
publishing. Tests verify correct broker connection and topic filtering per
channel grid configuration. All 30 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 07:45:00 +02:00
449a4c9398 feat(meshtastic): passive capture pipeline
Implement Task 6: PassiveCapture with packet recording (JSON lines),
node census tracking, and per-channel statistics. Payload withheld
when decrypted=False; included when decrypted=True.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-07-22 07:41:36 +02:00
bda0e1280e feat(meshtastic): grid policy + nft egress allow-list
- Implement targets_for(channel, cfg) to return grid membership subset
- Implement nft_egress_rules(cfg) to generate allow-rules for enabled on-grid brokers
- TDD: 5 test cases all passing (offgrid, shared+on, on-disabled, empty rules, broker rules)
- Full test suite: 23 passed

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 07:37:56 +02:00
bd2f9ac48c feat(meshtastic): RadioInterface + MockRadio + lazy serial
Implements Task 4: RadioInterface protocol with MockRadio test double and
SerialRadio wrapper. Lazy meshtastic import in open_serial() ensures test
suite never requires the library. Returns None when device absent (radio: absent
path). All tests pass; full suite clean (18/18).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 07:32:36 +02:00
e9d4559410 fix(meshtastic): task-3 review — deepcopy cache isolation + lock write + header (ref #897)
- Use copy.deepcopy in StateCache.get() and update() to prevent nested-mutable sharing
- Move _write_atomic() call inside lock in update() for atomicity
- Add copyright line to tests/test_cache.py header
- Add test_get_returns_deep_copy_not_live_reference() to verify isolation

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 07:30:30 +02:00
5477ad5fc8 feat(meshtastic): StateCache double-cache (in-mem + state.json + bg thread)
Add api/cache.py with StateCache class supporting:
- update(state_dict) for in-memory + atomic file write
- get() returning warm cache, file fallback, or {"radio": "absent"}
- start_refresh(producer, interval, stop) spawning lint-recognized
  threading.Thread(target=self._refresh_loop, ...) background refresher

Add tests/test_cache.py covering roundtrip persistence, cold read, missing
file fallback, and refresh thread lifecycle. All 14 suite tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 07:25:29 +02:00
8adb9cd3f0 feat(meshtastic): mesh state model + packet parser
TDD implementation: Packet dataclass with parse_packet() parser,
Node dataclass for mesh participants, MeshState with apply_packet()
and apply_nodeinfo() to build census and channel message logs.
Parser consumes meshtastic pubsub dict format.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 07:21:39 +02:00
0cc74b7be9 fix(meshtastic): task-1 review — debhelper Build-Depends + config KeyError guards (ref #897)
Changes:
1. debian/control: Add Build-Depends: debhelper-compat (= 13) and Rules-Requires-Root: no
2. debian/compat: Removed (now managed via Build-Depends)
3. api/config.py: Guard ch["name"] and sec["broker"] lookups against KeyError
4. tests/test_config.py: Add test_rejects_channel_without_name and test_rejects_broker_section_without_broker

All 6 tests pass (4 existing + 2 new). dpkg-checkbuilddeps reports no issues.
2026-07-22 07:19:55 +02:00
b89c79c0cc feat(meshtastic): package scaffold + config loader (ref #897)
Task 1 complete:
- Package scaffold with proper directory structure
- Config loader using tomllib with dataclass-based interface
- Comprehensive test coverage (4 tests, all passing)
- Example TOML configuration with sensible defaults
- Debian packaging (control, compat=13, changelog, rules)
- Full SPDX header compliance

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 07:14:12 +02:00