|
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>
|
||
|---|---|---|
| .. | ||
| api | ||
| conf | ||
| debian | ||
| menu.d | ||
| nginx | ||
| sbin | ||
| sudoers.d | ||
| systemd | ||
| tests | ||
| www/meshtastic | ||
| README.md | ||
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-runto escape the panel'sProtectSystem=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(groupdialoutfor serial access), never root. /etc/secubox/secrets/meshtastic/—0700, ownedsecubox-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 ownedsecubox-meshtastic; their shared parents (/etc/secubox,/var/cache/secubox,/var/log/secubox) stay0755root-owned and traversable by every othersecubox-*daemon — never narrowed (ref #511/#474).NoNewPrivileges,ProtectSystem=strict,ProtectHome=yes,PrivateTmp=yes;PrivateDevicesdeliberately not set (serial radio access needs/dev/ttyUSB*//dev/ttyACM*, viaDeviceAllow+dialout).- No
RuntimeDirectory=secuboxon the unit (would re-chown the shared/run/secuboxon every restart, breaking sibling sockets) — the daemon creates its own/run/secubox/meshtastic.sockdirectly.
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