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.
This commit is contained in:
CyberMind-FR 2026-07-22 08:40:31 +02:00
parent 6d5139ec7b
commit 340bb6a650
5 changed files with 171 additions and 0 deletions

View File

@ -0,0 +1,37 @@
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
#
# SecuBox-Deb :: secubox-meshtastic :: private "shared_grid" broker.
#
# Installed as /etc/mosquitto/conf.d/secubox-meshtastic.conf. This listener
# federates the "shared_grid" Meshtastic channel(s) between SecuBox nodes
# over the encrypted MirrorNet WireGuard mesh ONLY — it is bound to this
# node's mesh IP (10.10.0.1), never to 0.0.0.0/:: (see
# conf/meshtastic.toml.example: [shared_grid] broker = "10.10.0.1:1883").
# The "on_grid" bridge (public/community MQTT, e.g. mqtt.example.org) is a
# separate, independent client connection made BY this daemon and is not
# affected by this listener.
#
# This file ships the topology only. Mosquitto itself is NOT started by the
# secubox-meshtastic package (it's a shared system daemon other modules may
# also use) — the operator must:
# 1. apt install mosquitto mosquitto-clients
# 2. provision credentials: mosquitto_passwd -c /etc/mosquitto/secubox-meshtastic.passwd <user>
# 3. write /etc/mosquitto/secubox-meshtastic.acl (topic ACLs per node/user)
# 4. systemctl enable --now mosquitto
# Replace the default any-interface listener for this purpose: bind ONLY to
# the mesh address, on the standard MQTT port.
listener 1883 10.10.0.1
# No anonymous publish/subscribe on the mesh listener.
allow_anonymous false
# Password file — created by the operator (step 2 above). Left commented so
# a fresh install fails closed (mosquitto refuses to start rather than
# silently allowing anonymous access) until the operator provisions it.
#password_file /etc/mosquitto/secubox-meshtastic.passwd
# Optional per-topic ACL (recommended once multiple nodes/users share the
# grid) — created by the operator (step 3 above).
#acl_file /etc/mosquitto/secubox-meshtastic.acl

View File

@ -0,0 +1,50 @@
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
# Installed as /etc/systemd/system/secubox-meshtasticd.service — own socket,
# own service (never served by the aggregator: a radio/serial-attached daemon
# must not depend on something it may later restart, and the aggregator loop
# must not block on serial I/O).
[Unit]
Description=SecuBox Meshtastic — LoRa mesh node daemon (radio/cache/passive/bridge)
After=network.target secubox-core.service
Wants=secubox-core.service
[Service]
Type=simple
User=secubox-meshtastic
Group=secubox-meshtastic
# Serial device access (/dev/ttyUSB*, /dev/ttyACM*) is granted via the
# dialout group PLUS the explicit DeviceAllow rules below (belt+braces:
# some udev rulesets restrict ttyUSB/ttyACM perms to group dialout only,
# others rely on the cgroup device controller).
SupplementaryGroups=dialout
WorkingDirectory=/usr/lib/secubox/meshtastic
ExecStart=/usr/sbin/secubox-meshtasticd
Restart=on-failure
RestartSec=5
UMask=0007
# NB (ref #896): do NOT set `RuntimeDirectory=secubox` here. Any unit that
# declares RuntimeDirectory=secubox has systemd chown the SHARED /run/secubox
# directory to *this unit's* user on every (re)start — which breaks every
# other secubox-* service's socket bind the next time this unit restarts.
# /run/secubox is provided 1777 root:root by tmpfiles (secubox-core), i.e.
# world-writable + sticky, so this daemon creates its own
# /run/secubox/meshtastic.sock there directly, with no RuntimeDirectory at
# all. If a private scratch dir is ever needed, use
# `RuntimeDirectory=secubox-meshtastic` (this unit's OWN dir) — never
# `=secubox`.
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/run/secubox /var/cache/secubox/meshtastic /var/log/secubox/meshtastic
# Serial radio access — PrivateDevices=yes would hide /dev/ttyUSB*/ttyACM*
# entirely, so it is deliberately NOT set here.
DeviceAllow=char-ttyUSB rw
DeviceAllow=char-ttyACM rw
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,9 @@
{
"id": "meshtastic",
"name": "Meshtastic",
"category": "mesh",
"icon": "📡",
"path": "/meshtastic/",
"order": 71,
"description": "Meshtastic LoRa mesh node — radio status, channels and grid bridging"
}

View File

@ -0,0 +1,25 @@
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
# /etc/nginx/secubox-routes.d/meshtastic.conf — installé par secubox-meshtastic.
#
# Ce répertoire est celui que le vhost admin inclut réellement
# (`include /etc/nginx/secubox-routes.d/*.conf;`).
# Panel (statique, servi par le vhost admin).
location /meshtastic/ {
alias /usr/share/secubox/www/meshtastic/;
try_files $uri $uri/ /meshtastic/index.html;
}
# API — socket DÉDIÉE, jamais l'aggregator : le daemon radio/série ne doit
# pas dépendre de ce qu'il est susceptible de redémarrer, et la boucle de
# l'aggregator ne doit pas bloquer sur de l'I/O série (cf. api/daemon.py).
#
# PAS de `:/` en fin de proxy_pass : l'app enregistre le chemin COMPLET
# (@app.get("/api/v1/meshtastic/...")), donc le préfixe doit être préservé.
# Le stripper renverrait un chemin tronqué à une app qui ne connaît que
# /api/v1/meshtastic/... → 404.
location /api/v1/meshtastic/ {
proxy_pass http://unix:/run/secubox/meshtastic.sock;
include /etc/nginx/snippets/secubox-proxy.conf;
}

View File

@ -0,0 +1,50 @@
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
# Installed as /etc/systemd/system/secubox-meshtasticd.service — own socket,
# own service (never served by the aggregator: a radio/serial-attached daemon
# must not depend on something it may later restart, and the aggregator loop
# must not block on serial I/O).
[Unit]
Description=SecuBox Meshtastic — LoRa mesh node daemon (radio/cache/passive/bridge)
After=network.target secubox-core.service
Wants=secubox-core.service
[Service]
Type=simple
User=secubox-meshtastic
Group=secubox-meshtastic
# Serial device access (/dev/ttyUSB*, /dev/ttyACM*) is granted via the
# dialout group PLUS the explicit DeviceAllow rules below (belt+braces:
# some udev rulesets restrict ttyUSB/ttyACM perms to group dialout only,
# others rely on the cgroup device controller).
SupplementaryGroups=dialout
WorkingDirectory=/usr/lib/secubox/meshtastic
ExecStart=/usr/sbin/secubox-meshtasticd
Restart=on-failure
RestartSec=5
UMask=0007
# NB (ref #896): do NOT set `RuntimeDirectory=secubox` here. Any unit that
# declares RuntimeDirectory=secubox has systemd chown the SHARED /run/secubox
# directory to *this unit's* user on every (re)start — which breaks every
# other secubox-* service's socket bind the next time this unit restarts.
# /run/secubox is provided 1777 root:root by tmpfiles (secubox-core), i.e.
# world-writable + sticky, so this daemon creates its own
# /run/secubox/meshtastic.sock there directly, with no RuntimeDirectory at
# all. If a private scratch dir is ever needed, use
# `RuntimeDirectory=secubox-meshtastic` (this unit's OWN dir) — never
# `=secubox`.
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/run/secubox /var/cache/secubox/meshtastic /var/log/secubox/meshtastic
# Serial radio access — PrivateDevices=yes would hide /dev/ttyUSB*/ttyACM*
# entirely, so it is deliberately NOT set here.
DeviceAllow=char-ttyUSB rw
DeviceAllow=char-ttyACM rw
[Install]
WantedBy=multi-user.target