secubox-deb/packages/secubox-meshtastic
CyberMind-FR db0ebe1675
Some checks are pending
License Headers / check (push) Waiting to run
fix(meshtastic): ExecStartPre clears stale socket so the daemon survives restart (ref #897)
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
..
api fix(meshtastic): daemon survives an unreachable MQTT broker at startup (ref #897) 2026-07-22 10:36:42 +02:00
conf feat(meshtastic): systemd + nginx + menu + private broker (ref #897) 2026-07-22 08:40:31 +02:00
debian fix(meshtastic): ExecStartPre clears stale socket so the daemon survives restart (ref #897) 2026-07-22 10:46:56 +02:00
menu.d feat(meshtastic): systemd + nginx + menu + private broker (ref #897) 2026-07-22 08:40:31 +02:00
nginx feat(meshtastic): systemd + nginx + menu + private broker (ref #897) 2026-07-22 08:40:31 +02:00
sbin feat(meshtastic): secubox-meshtasticctl + sudoers (ref #897) 2026-07-22 08:02:29 +02:00
sudoers.d fix(meshtastic): final review — panel webui→ctl wiring (NNP+sudoers user) + mosquitto Recommends + minors (ref #897) 2026-07-22 09:02:55 +02:00
systemd fix(meshtastic): ExecStartPre clears stale socket so the daemon survives restart (ref #897) 2026-07-22 10:46:56 +02:00
tests fix(meshtastic): daemon survives an unreachable MQTT broker at startup (ref #897) 2026-07-22 10:36:42 +02:00
www/meshtastic feat(meshtastic): webui panel (ref #897) 2026-07-22 08:32:35 +02:00
README.md feat(meshtastic): debian packaging + README (ref #897) 2026-07-22 08:50:56 +02:00

secubox-meshtastic

Multi-grid Meshtastic LoRa mesh node + passive RF listener for SecuBox-Deb. Turns the box into a Meshtastic participant driving a USB-attached LoRa device across three composable "grids", and/or a passive observe-only listener that feeds the SOC. Full design: docs/superpowers/specs/2026-07-22-secubox-meshtastic-multigrid-design.md, tracked in #897.

Hardware bring-up (flashing, /dev/tty* passthrough, real EU868 RX/TX, live shared-grid federation between two nodes) is deferred — no radio has shipped yet (#897). Everything in this package is fully testable and runnable against MockRadio/FakeMqtt with zero hardware: the daemon reports radio: absent and keeps serving cache/passive/bridge/webui.

Architecture

A native-host daemon (secubox-meshtasticd, own systemd unit, own Unix socket — never served by the aggregator: a serial-attached daemon must not depend on something it may later restart, and the aggregator loop must not block on serial I/O) owns the serial device, maintains live mesh state behind the double-cache pattern (in-memory + /var/cache/secubox/meshtastic/state.json

  • background refresh thread), and performs host-side serial↔MQTT bridging so SecuBox — not the device firmware — enforces egress, WAF routing and per-channel grid policy. A FastAPI webui panel (/meshtastic/) reads the cache and sends real-time actions directly; privileged config changes are delegated to a confined root CLI, secubox-meshtasticctl (webui→ctl pattern, sudoers-scoped, systemd-run to escape the panel's ProtectSystem=strict).

The three grids

Each Meshtastic channel independently selects its grid(s) (per-channel grid policy, like the Punk Exposure engine's Tor/DNS/Mesh channels):

Grid Mechanism Transport Default
off (off-grid) LoRa RF managed-flood mesh radio on, when a device is present
shared (shared-grid, private) serial↔MQTT bridge to a private broker WireGuard MirrorNet (10.10.0.0/24) opt-in per channel
on (on-grid, public) serial↔MQTT bridge to a public broker Internet, WAF/mitmproxy + nft egress-locked off — opt-in per channel

Public (on) egress is nft DEFAULT DROP with an explicit allow-list generated by secubox-meshtasticctl apply-egress from the enabled on-grid brokers only (fail-safe: an empty ruleset opens nothing). No waf_bypass — all public egress is routed through the box's WAF/mitmproxy.

Passive listener mode

Set mode = "passive-listener" (or "both") and [passive].role = "CLIENT_MUTE" (receive-only, no relay/announce). Every received packet is appended to /var/log/secubox/meshtastic/packets.jsonl (append-only, RFC-3339 timestamps), alongside a passive node census and per-channel activity stats, all served at GET /api/v1/meshtastic/packets. Channels whose PSK is known are decoded; all others are logged metadata-only from the cleartext LoRa header.

API (/api/v1/meshtastic, JWT-protected, own socket)

Method Path Description
GET /status Full mesh snapshot (radio presence, mode, nodes, messages, census, channel stats)
GET /nodes Node list
GET /messages Messages grouped by channel
GET /packets Passive census + per-channel packet stats
POST /send {channel, text} — send a text message (real-time, not delegated to ctl)
POST /mode {mode}active-node / passive-listener / both (delegates to ctl, validated against the same enum first)
POST /grid {channel, grid} — set a channel's grid list (delegates to ctl, validated first)

secubox-meshtasticctl (root only, via sudoers systemd-run) additionally exposes set-region, set-role, set-psk (sets the PSK reference name only — never secret bytes) and apply-egress (regenerates the nft allow-list). Every write is shadow → validate → atomic-swap (double-buffer / 4R) and appended to /var/log/secubox/audit.log.

Configuration — /etc/secubox/meshtastic.toml

Seeded from /usr/share/secubox/meshtastic/meshtastic.toml.example on first install only (an existing operator-edited file is never overwritten).

mode = "both"                 # active-node | passive-listener | both
region = "EU_868"
serial = "auto"                # or an explicit /dev/ttyUSB0 / /dev/ttyACM0

[[channel]]
name = "family"
grid = ["off", "shared"]       # any of: off, shared, on
psk_secret = "family-psk"      # reference NAME only — bytes live elsewhere

[shared_grid]
broker = "10.10.0.1:1883"       # MirrorNet mesh IP only, never 0.0.0.0

[on_grid]
broker = "mqtt.example.org:8883"
enabled = false                 # opt-in — off by default

[passive]
role = "CLIENT_MUTE"
packet_log = "/var/log/secubox/meshtastic/packets.jsonl"

Private shared-grid broker (mosquitto)

/usr/share/secubox/meshtastic/mosquitto-secubox-meshtastic.conf ships a reference listener config (bound to the MirrorNet mesh IP only, anonymous access disabled). It is not installed automatically into /etc/mosquitto/conf.d/ — copy it in deliberately when enabling shared-grid:

apt install mosquitto mosquitto-clients
install -m 0644 /usr/share/secubox/meshtastic/mosquitto-secubox-meshtastic.conf \
    /etc/mosquitto/conf.d/secubox-meshtastic.conf
mosquitto_passwd -c /etc/mosquitto/secubox-meshtastic.passwd <user>
# write /etc/mosquitto/secubox-meshtastic.acl, then uncomment
# password_file/acl_file in the installed conf.d file
systemctl enable --now mosquitto

The meshtastic pip dependency

The upstream meshtastic Python library (device/BLE/serial protocol, SerialInterface + pubsub) is not packaged for Debian and is deliberately not a Depends: of this package. api/radio.py imports it lazily inside open_serial() — with no device attached, or the library absent, the daemon simply reports radio: absent and keeps running (cache/passive/bridge/webui unaffected). Install it only once a physical device is attached:

python3 -m venv /opt/secubox-meshtastic-venv   # or system pip with --break-system-packages
/opt/secubox-meshtastic-venv/bin/pip install meshtastic

(A future task may wire an explicit venv path into the unit; today open_serial() resolves meshtastic from whatever python3 sees, so a system-wide pip install --break-system-packages meshtastic also works.)

Permissions / privilege separation

  • Dedicated system user secubox-meshtastic (group dialout for serial access), never root.
  • /etc/secubox/secrets/meshtastic/0700, owned secubox-meshtastic (PSKs / MQTT credentials, referenced by name from the TOML config, never stored in it).
  • /var/cache/secubox/meshtastic/ and /var/log/secubox/meshtastic/ — leaf dirs owned secubox-meshtastic; their shared parents (/etc/secubox, /var/cache/secubox, /var/log/secubox) stay 0755 root-owned and traversable by every other secubox-* daemon — never narrowed (ref #511/#474).
  • NoNewPrivileges, ProtectSystem=strict, ProtectHome=yes, PrivateTmp=yes; PrivateDevices deliberately not set (serial radio access needs /dev/ttyUSB*//dev/ttyACM*, via DeviceAllow + dialout).
  • No RuntimeDirectory=secubox on the unit (would re-chown the shared /run/secubox on every restart, breaking sibling sockets) — the daemon creates its own /run/secubox/meshtastic.sock directly.

Testing

cd packages/secubox-meshtastic
python -m pytest tests/ -q

All 60 tests run against MockRadio + a fake MQTT client — no hardware, no serial port, no meshtastic pip package required.

Building

cd packages/secubox-meshtastic
dpkg-buildpackage -us -uc -b