feat(secubox-mesh): Add OpenWrt mesh daemon with topology management

Port secuboxd from Debian/Go to OpenWrt shell implementation:
- secuboxd daemon with Unix control socket at /var/run/secuboxd/topo.sock
- secuboxctl CLI compatible with Debian version interface
- Mesh libraries: topology, discovery, election, telemetry, control
- Mesh gate election with weighted scoring (uptime, peers, CPU, memory, role)
- mDNS service discovery (_secubox._udp.local) via umdns
- DID integration via mirrornet identity library
- RPCD handler with 11 ubus methods for LuCI integration
- procd init script with respawn and network triggers
- UCI config sections: mesh, node, telemetry, discovery

Fixes subprocess state access for socat handler by saving daemon state to file.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-03-26 06:27:45 +01:00
parent 707142c6bb
commit cd6af3edff
15 changed files with 2062 additions and 2 deletions

View File

@ -1,6 +1,37 @@
# SecuBox UI & Theme History
_Last updated: 2026-03-20 (Wiki Translations & Meta-package)_
_Last updated: 2026-03-25 (CRT P31 Phosphor Theme Enhancement)_
0. **CRT P31 Phosphor Theme Enhancement (2026-03-25)**
- **NEW: CRT P31 theme variant** (`themes/crt-p31.css`)
- Authentic P31 phosphor green CRT terminal aesthetic
- Color palette: peak (#33ff66), hot (#66ffaa), mid (#22cc44), dim (#0f8822), ghost (#052210)
- Phosphor decay amber for warnings (#ffb347)
- CRT tube blacks for backgrounds (#050803, #080d05, #0d1208)
- **CRT visual effects** (`htdocs/luci-static/secubox/`)
- `cascade.css`: Complete CRT theme as standalone LuCI theme
- `crt-engine.js`: Scanlines overlay, phosphor glow, boot sequence animation
- `crt-components.js`: Reusable widgets, badges, progress bars, topology nodes
- **LuCI view templates** (`luasrc/luci/view/themes/secubox/`)
- `header.htm`: CRT-styled header with hostname display
- `footer.htm`: Mesh version and branding
- `sysauth.htm`: Terminal-style login page with glowing elements
- **Theme features**:
- Scanlines overlay (CSS pseudo-element, pointer-events: none)
- Phosphor bloom effects on text and interactive elements
- Monospace font stack (Courier Prime, IBM Plex Mono, Fira Code)
- Terminal boot sequence animation on first visit
- Status indicators with appropriate glow colors
- **Integration**:
- Added to existing secubox-theme system via `data-secubox-theme="crt-p31"`
- Compatible with all SecuBox LuCI modules
- Set as default theme via UCI defaults script
- **Files created/updated**:
- `htdocs/luci-static/resources/secubox-theme/themes/crt-p31.css`
- `htdocs/luci-static/secubox/cascade.css`
- `htdocs/luci-static/secubox/crt-engine.js`
- `htdocs/luci-static/secubox/crt-components.js`
- Makefile updated to PKG_RELEASE:=2
0. **Wiki Internationalization & Meta-package (2026-03-20)**
- **Wiki translations**: All 17 wiki pages translated to French and Chinese
@ -5433,3 +5464,32 @@ git checkout HEAD -- index.html
- Migrated secubox-reporter and bandwidth-manager to use shared library
- Backwards-compatible fallback to legacy per-app SMTP settings
- Eliminates duplicated SMTP configuration across SecuBox apps
### 2026-03-25
- **SecuBox Mesh Daemon (`secubox-mesh`) (Complete)**
- New `secubox-mesh` package: OpenWrt-native mesh daemon ported from Debian/Go version
- **secuboxd** daemon with Unix control socket at `/var/run/secuboxd/topo.sock`
- **secuboxctl** CLI tool compatible with Debian version interface:
- `mesh status|peers|topology|nodes` - mesh operations
- `node info|rotate` - node identity management
- `telemetry latest` - system metrics
- `start|stop|restart` - service control
- **Libraries** in `/usr/lib/secubox-mesh/`:
- `topology.sh` - node/edge management, graph storage, pruning
- `discovery.sh` - mDNS, WireGuard, ARP, static peer discovery
- `election.sh` - mesh gate election with weighted scoring algorithm
- `telemetry.sh` - system metrics collection (CPU, memory, uptime, load)
- `control.sh` - socket command parsing, rate limiting, health checks
- **Mesh Gate Election Algorithm**:
- Scoring weights: uptime (30%), peers (25%), CPU (15%), memory (15%), role (15%)
- Relay nodes preferred over edge nodes for gate role
- Leader election via highest score, periodic re-election
- **mDNS Service Discovery**: `_secubox._udp.local` with TXT records (did, role, version)
- **DID Integration**: Uses existing mirrornet identity library for did:plc format
- **UCI Config**: `/etc/config/secubox` with mesh, node, telemetry, discovery sections
- **procd Init Script**: Respawn, network triggers, file triggers
- **RPCD Handler**: `luci.secubox-mesh` with 11 methods for ubus access
- **ACL Permissions**: Read (status, peers, topology, nodes, telemetry, ping, get_config), Write (node_rotate, set_config, restart)
- **Dependencies**: secubox-mirrornet, secubox-identity, libubox-lua, libubus-lua, umdns, wireguard-tools, jshn, socat
- Cross-platform compatible with Debian secuboxd for hybrid mesh networks

View File

@ -1,6 +1,6 @@
# Work In Progress (Claude)
_Last updated: 2026-03-17 (VM Firmware Build + CI Fixes)_
_Last updated: 2026-03-25 (SecuBox Mesh Daemon)_
> **Architecture Reference**: SecuBox Fanzine v3 — Les 4 Couches
@ -8,6 +8,17 @@ _Last updated: 2026-03-17 (VM Firmware Build + CI Fixes)_
## Recently Completed
### 2026-03-25
- **SecuBox Mesh Daemon (`secubox-mesh`) (Complete)**
- OpenWrt-native mesh daemon ported from Debian/Go version
- `secuboxd` daemon with Unix control socket, `secuboxctl` CLI
- Libraries: topology, discovery, election, telemetry, control
- Mesh gate election with weighted scoring algorithm
- mDNS service discovery: `_secubox._udp.local`
- RPCD handler with 11 ubus methods
- Cross-platform compatible with Debian version
### 2026-03-17
- **SecuBox VM Firmware Build Workflow (Complete)**

View File

@ -0,0 +1,60 @@
# SPDX-License-Identifier: MIT
# SecuBox Mesh Daemon for OpenWrt
# CyberMind — SecuBox — 2026
include $(TOPDIR)/rules.mk
PKG_NAME:=secubox-mesh
PKG_VERSION:=1.0.0
PKG_RELEASE:=1
PKG_MAINTAINER:=Gerald KERMA <devel@cybermind.fr>
PKG_LICENSE:=MIT
include $(INCLUDE_DIR)/package.mk
define Package/secubox-mesh
SECTION:=net
CATEGORY:=Network
SUBMENU:=SecuBox
TITLE:=SecuBox Mesh Daemon
DEPENDS:=+secubox-mirrornet +secubox-identity +libubox-lua +libubus-lua +umdns +wireguard-tools +jshn
PKGARCH:=all
endef
define Package/secubox-mesh/description
SecuBox mesh daemon (secuboxd) for OpenWrt providing:
- Unix control socket at /var/run/secuboxd/topo.sock
- mDNS service discovery (_secubox._udp)
- Mesh topology management with gate election
- Compatible with Debian secuboxd for cross-platform mesh
- CLI tool: secuboxctl
endef
define Build/Compile
endef
define Package/secubox-mesh/conffiles
/etc/config/secubox
endef
define Package/secubox-mesh/install
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) ./files/usr/sbin/secuboxd $(1)/usr/sbin/
$(INSTALL_BIN) ./files/usr/sbin/secuboxctl $(1)/usr/sbin/
$(INSTALL_DIR) $(1)/usr/lib/secubox-mesh
$(INSTALL_DATA) ./files/usr/lib/secubox-mesh/topology.sh $(1)/usr/lib/secubox-mesh/
$(INSTALL_DATA) ./files/usr/lib/secubox-mesh/discovery.sh $(1)/usr/lib/secubox-mesh/
$(INSTALL_DATA) ./files/usr/lib/secubox-mesh/telemetry.sh $(1)/usr/lib/secubox-mesh/
$(INSTALL_DATA) ./files/usr/lib/secubox-mesh/control.sh $(1)/usr/lib/secubox-mesh/
$(INSTALL_DATA) ./files/usr/lib/secubox-mesh/election.sh $(1)/usr/lib/secubox-mesh/
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) ./files/etc/config/secubox $(1)/etc/config/
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) ./files/etc/init.d/secuboxd $(1)/etc/init.d/
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
$(INSTALL_BIN) ./files/usr/libexec/rpcd/luci.secubox-mesh $(1)/usr/libexec/rpcd/
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-secubox-mesh.json $(1)/usr/share/rpcd/acl.d/
endef
$(eval $(call BuildPackage,secubox-mesh))

View File

@ -0,0 +1,33 @@
config mesh 'mesh'
option enabled '1'
option role 'edge'
option subnet '10.42.0.0/16'
option mdns_service '_secubox._udp'
option beacon_interval '30'
option election_interval '60'
option prune_interval '300'
config node 'node'
option did ''
option keypair '/var/lib/mirrornet/identity/keys/primary.key'
option auto_rotate '0'
option rotate_days '30'
config telemetry 'telemetry'
option enabled '1'
option interval '60'
option share_metrics '1'
config discovery 'discovery'
option mdns '1'
option wireguard '1'
option arp '1'
option static '1'
# Example static peer configuration
#config peer 'peer1'
# option did 'did:plc:abcdef0123456789'
# option address '192.168.1.100'
# option role 'relay'
# option port '51820'

View File

@ -0,0 +1,77 @@
#!/bin/sh /etc/rc.common
# SecuBox Mesh Daemon init script
# CyberMind — SecuBox — 2026
START=95
STOP=10
USE_PROCD=1
PROG=/usr/sbin/secuboxd
PIDFILE=/var/run/secuboxd/secuboxd.pid
start_service() {
config_load secubox
local enabled
config_get_bool enabled mesh enabled 1
[ "$enabled" -eq 0 ] && {
echo "secuboxd is disabled"
return 0
}
# Create required directories
mkdir -p /var/run/secuboxd
mkdir -p /var/lib/secubox-mesh
mkdir -p /var/log
procd_open_instance secuboxd
procd_set_param command "$PROG" --foreground
procd_set_param respawn 3600 5 5
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param pidfile "$PIDFILE"
# Reload on network changes
procd_set_param netdev br-lan wg0
procd_set_param file /etc/config/secubox
procd_close_instance
}
stop_service() {
# Clean up socket
rm -f /var/run/secuboxd/topo.sock
# Kill any remaining processes
killall -q secuboxd 2>/dev/null
}
reload_service() {
stop
start
}
service_triggers() {
procd_add_reload_trigger "secubox" "network"
}
status() {
local pid
pid=$(cat "$PIDFILE" 2>/dev/null)
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
echo "secuboxd is running (PID: $pid)"
# Show quick status
if [ -S /var/run/secuboxd/topo.sock ]; then
echo "Socket: /var/run/secuboxd/topo.sock (active)"
secuboxctl mesh status 2>/dev/null
fi
return 0
else
echo "secuboxd is not running"
return 1
fi
}

View File

@ -0,0 +1,138 @@
#!/bin/sh
# SecuBox Mesh Control Socket Handler
# Handles commands received via Unix control socket
# CyberMind — SecuBox — 2026
# Control socket helpers
# Parse JSON-RPC style command
control_parse_command() {
local input="$1"
# Simple command (just method name)
if ! echo "$input" | grep -q '{'; then
echo "$input"
return
fi
# JSON-RPC format: {"method":"...", "params":{...}}
local method
method=$(echo "$input" | jsonfilter -e '@.method' 2>/dev/null)
echo "$method"
}
# Get command parameters
control_get_params() {
local input="$1"
echo "$input" | jsonfilter -e '@.params' 2>/dev/null || echo '{}'
}
# Format JSON response
control_response() {
local result="$1"
local id="${2:-null}"
cat <<EOF
{"jsonrpc":"2.0","result":$result,"id":$id}
EOF
}
# Format JSON error
control_error() {
local code="$1"
local message="$2"
local id="${3:-null}"
cat <<EOF
{"jsonrpc":"2.0","error":{"code":$code,"message":"$message"},"id":$id}
EOF
}
# Validate command authorization (placeholder for future auth)
control_authorize() {
local cmd="$1"
local client="$2"
# All commands allowed for now (socket access = authorized)
return 0
}
# Log control socket activity
control_log() {
local level="$1"
local msg="$2"
logger -t secuboxd-control -p "daemon.$level" "$msg"
}
# Rate limiting (simple token bucket)
RATE_LIMIT_FILE="/tmp/secuboxd_rate_limit"
RATE_LIMIT_MAX=100 # Max requests per minute
RATE_LIMIT_WINDOW=60
control_check_rate_limit() {
local client="$1"
local current_time
current_time=$(date +%s)
if [ ! -f "$RATE_LIMIT_FILE" ]; then
echo "$current_time:1" > "$RATE_LIMIT_FILE"
return 0
fi
local last_time last_count
IFS=':' read -r last_time last_count < "$RATE_LIMIT_FILE"
local elapsed=$((current_time - last_time))
if [ "$elapsed" -ge "$RATE_LIMIT_WINDOW" ]; then
# Reset window
echo "$current_time:1" > "$RATE_LIMIT_FILE"
return 0
fi
if [ "$last_count" -ge "$RATE_LIMIT_MAX" ]; then
control_log "warn" "Rate limit exceeded for $client"
return 1
fi
echo "$last_time:$((last_count + 1))" > "$RATE_LIMIT_FILE"
return 0
}
# Available commands and their handlers
control_list_commands() {
cat <<EOF
{
"commands": [
{"method": "ping", "description": "Check daemon is alive"},
{"method": "mesh.status", "description": "Get mesh status"},
{"method": "mesh.peers", "description": "List connected peers"},
{"method": "mesh.topology", "description": "Get mesh topology graph"},
{"method": "mesh.nodes", "description": "List all known nodes"},
{"method": "node.info", "description": "Get this node's information"},
{"method": "node.rotate", "description": "Rotate node keys"},
{"method": "telemetry.latest", "description": "Get latest telemetry"}
]
}
EOF
}
# Health check for socket server
control_health_check() {
local socket="$1"
if [ ! -S "$socket" ]; then
return 1
fi
# Try sending ping
local response
response=$(echo "ping" | timeout 2 socat - "UNIX-CONNECT:$socket" 2>/dev/null)
if echo "$response" | grep -q '"pong":true'; then
return 0
fi
return 1
}

View File

@ -0,0 +1,215 @@
#!/bin/sh
# SecuBox Mesh Peer Discovery
# mDNS-based service discovery for mesh peers
# CyberMind — SecuBox — 2026
PEERS_FILE="/var/lib/secubox-mesh/peers.json"
DISCOVERY_CACHE="/tmp/secubox_discovery_cache"
MDNS_SERVICE="_secubox._udp"
# Initialize discovery
discovery_init() {
mkdir -p "$(dirname "$PEERS_FILE")"
[ -f "$PEERS_FILE" ] || echo '[]' > "$PEERS_FILE"
}
# Scan for peers using mDNS
discovery_scan_mdns() {
local peers=""
# Try umdns-client first
if command -v umdns >/dev/null 2>&1; then
# Query umdns for _secubox._udp services
local services
services=$(ubus call umdns browse 2>/dev/null | jsonfilter -e '@.services[*]' 2>/dev/null)
echo "$services" | while IFS= read -r svc; do
local name addr port txt_did txt_role
name=$(echo "$svc" | jsonfilter -e '@.name' 2>/dev/null)
addr=$(echo "$svc" | jsonfilter -e '@.address' 2>/dev/null)
port=$(echo "$svc" | jsonfilter -e '@.port' 2>/dev/null)
# Parse TXT records
txt_did=$(echo "$svc" | jsonfilter -e '@.txt[*]' 2>/dev/null | grep "^did=" | cut -d= -f2)
txt_role=$(echo "$svc" | jsonfilter -e '@.txt[*]' 2>/dev/null | grep "^role=" | cut -d= -f2)
[ -n "$txt_did" ] && echo "$txt_did|$addr|${txt_role:-edge}|$port"
done
fi
# Try avahi-browse as fallback
if command -v avahi-browse >/dev/null 2>&1; then
avahi-browse -rpt "$MDNS_SERVICE.local" 2>/dev/null | \
grep "^=" | while IFS=';' read -r _ _ _ name _ _ addr port txt; do
local did role
did=$(echo "$txt" | grep -o 'did=[^"]*' | cut -d= -f2 | tr -d '"')
role=$(echo "$txt" | grep -o 'role=[^"]*' | cut -d= -f2 | tr -d '"')
[ -n "$did" ] && echo "$did|$addr|${role:-edge}|$port"
done
fi
}
# Scan for peers using WireGuard peer list
discovery_scan_wireguard() {
local wg_interface="${1:-wg0}"
if command -v wg >/dev/null 2>&1; then
wg show "$wg_interface" endpoints 2>/dev/null | while read -r pubkey endpoint; do
[ -z "$endpoint" ] || [ "$endpoint" = "(none)" ] && continue
local addr port
addr=$(echo "$endpoint" | cut -d: -f1)
port=$(echo "$endpoint" | cut -d: -f2)
# Generate DID from public key
local did
did="did:plc:$(echo -n "$pubkey" | md5sum | cut -c1-16)"
echo "$did|$addr|peer|$port"
done
fi
}
# Scan for peers using ARP/neighbor discovery
discovery_scan_arp() {
# Look for SecuBox nodes on local network via known ports
local secubox_port=7331 # P2P API port
ip neigh show 2>/dev/null | grep -v FAILED | while read -r ip _ _ mac _; do
[ -z "$ip" ] && continue
# Quick check if SecuBox P2P API is responding
local response
response=$(timeout 1 wget -qO- "http://$ip:$secubox_port/api/status" 2>/dev/null)
if [ -n "$response" ]; then
local did role
did=$(echo "$response" | jsonfilter -e '@.did' 2>/dev/null)
role=$(echo "$response" | jsonfilter -e '@.role' 2>/dev/null || echo "edge")
[ -n "$did" ] && echo "$did|$ip|$role|$secubox_port"
fi
done
}
# Scan for peers using configured peer list
discovery_scan_config() {
# Read static peers from UCI config
config_load secubox
config_foreach _scan_config_peer peer
}
_scan_config_peer() {
local section="$1"
local did addr role port
config_get did "$section" did ""
config_get addr "$section" address ""
config_get role "$section" role "edge"
config_get port "$section" port "51820"
[ -n "$did" ] && [ -n "$addr" ] && echo "$did|$addr|$role|$port"
}
# Combined peer discovery
discovery_scan_peers() {
local tmp_peers="/tmp/secubox_peers_$$.txt"
local seen_dids=""
# Combine all discovery methods
{
discovery_scan_mdns
discovery_scan_wireguard
discovery_scan_arp
discovery_scan_config
} | sort -u > "$tmp_peers"
# Build peers JSON
local peers_json='['
local first=1
local my_did="${NODE_DID:-$(cat /var/lib/mirrornet/identity/did.txt 2>/dev/null)}"
while IFS='|' read -r did addr role port; do
[ -z "$did" ] && continue
# Skip self
[ "$did" = "$my_did" ] && continue
# Skip duplicates
echo "$seen_dids" | grep -q "$did" && continue
seen_dids="$seen_dids $did"
[ "$first" = "1" ] || peers_json="$peers_json,"
local last_seen
last_seen=$(date -Iseconds)
peers_json="$peers_json{\"did\":\"$did\",\"address\":\"$addr\",\"role\":\"$role\",\"port\":$port,\"last_seen\":\"$last_seen\"}"
first=0
done < "$tmp_peers"
peers_json="$peers_json]"
# Save to peers file
echo "$peers_json" > "$PEERS_FILE"
rm -f "$tmp_peers"
}
# Get peer count
discovery_get_peer_count() {
jsonfilter -i "$PEERS_FILE" -e '@[*]' 2>/dev/null | wc -l
}
# Get peer by DID
discovery_get_peer() {
local did="$1"
jsonfilter -i "$PEERS_FILE" -e "@[did=\"$did\"]" 2>/dev/null
}
# Get all peers
discovery_get_peers() {
cat "$PEERS_FILE" 2>/dev/null || echo '[]'
}
# Check peer connectivity
discovery_check_peer() {
local did="$1"
local addr
addr=$(discovery_get_peer "$did" | jsonfilter -e '@.address' 2>/dev/null)
[ -z "$addr" ] && return 1
# Try ping
ping -c 1 -W 1 "$addr" >/dev/null 2>&1
}
# Refresh peer status
discovery_refresh_peer() {
local did="$1"
local peer_json
peer_json=$(discovery_get_peer "$did")
[ -z "$peer_json" ] && return 1
local addr port
addr=$(echo "$peer_json" | jsonfilter -e '@.address' 2>/dev/null)
port=$(echo "$peer_json" | jsonfilter -e '@.port' 2>/dev/null || echo "7331")
# Query peer's status API
local response
response=$(timeout 2 wget -qO- "http://$addr:$port/api/status" 2>/dev/null)
if [ -n "$response" ]; then
# Update peer info
local new_role
new_role=$(echo "$response" | jsonfilter -e '@.role' 2>/dev/null)
[ -n "$new_role" ] && logger -t secubox-mesh "Peer $did role updated: $new_role"
return 0
fi
return 1
}
# Initialize on source
discovery_init

View File

@ -0,0 +1,184 @@
#!/bin/sh
# SecuBox Mesh Gate Election
# Elects a mesh gate (coordinator) based on node capabilities
# CyberMind — SecuBox — 2026
ELECTION_STATE="/var/lib/secubox-mesh/election_state.json"
MESH_GATE_FILE="/var/lib/secubox-mesh/mesh_gate"
# Election criteria weights
WEIGHT_UPTIME=30 # Higher uptime = more stable
WEIGHT_PEERS=25 # More peers = better connected
WEIGHT_CPU=15 # Lower CPU = more available
WEIGHT_MEMORY=15 # Lower memory = more available
WEIGHT_ROLE=15 # relay > edge for gate role
# Initialize election state
election_init() {
mkdir -p "$(dirname "$ELECTION_STATE")"
}
# Calculate node score for gate election
election_calculate_score() {
local did="$1"
local telemetry="$2"
local peer_count="${3:-0}"
local role="${4:-edge}"
local score=0
# Uptime score (max 30 points for 24h+ uptime)
local uptime
uptime=$(echo "$telemetry" | jsonfilter -e '@.uptime' 2>/dev/null || echo 0)
local uptime_hours=$((uptime / 3600))
local uptime_score=$((uptime_hours > 24 ? WEIGHT_UPTIME : uptime_hours * WEIGHT_UPTIME / 24))
score=$((score + uptime_score))
# Peer count score (max 25 points for 10+ peers)
local peer_score=$((peer_count > 10 ? WEIGHT_PEERS : peer_count * WEIGHT_PEERS / 10))
score=$((score + peer_score))
# CPU availability score (lower usage = higher score)
local cpu_percent
cpu_percent=$(echo "$telemetry" | jsonfilter -e '@.cpu_percent' 2>/dev/null || echo 50)
local cpu_score=$(( (100 - cpu_percent) * WEIGHT_CPU / 100 ))
score=$((score + cpu_score))
# Memory availability score (lower usage = higher score)
local memory_percent
memory_percent=$(echo "$telemetry" | jsonfilter -e '@.memory_percent' 2>/dev/null || echo 50)
local memory_score=$(( (100 - memory_percent) * WEIGHT_MEMORY / 100 ))
score=$((score + memory_score))
# Role score
local role_score=0
case "$role" in
relay|master)
role_score=$WEIGHT_ROLE
;;
submaster)
role_score=$((WEIGHT_ROLE * 3 / 4))
;;
edge|peer)
role_score=$((WEIGHT_ROLE / 2))
;;
*)
role_score=$((WEIGHT_ROLE / 4))
;;
esac
score=$((score + role_score))
echo "$score"
}
# Run gate election
election_run() {
local nodes_file="/var/lib/secubox-mesh/nodes.json"
local telemetry_file="/var/lib/secubox-mesh/telemetry.json"
local peers_file="/var/lib/secubox-mesh/peers.json"
local best_did=""
local best_score=0
local my_did="${NODE_DID:-$(cat /var/lib/mirrornet/identity/did.txt 2>/dev/null)}"
# Calculate score for this node
if [ -n "$my_did" ]; then
local my_telemetry
my_telemetry=$(cat "$telemetry_file" 2>/dev/null || echo '{}')
local my_peer_count
my_peer_count=$(jsonfilter -i "$peers_file" -e '@[*]' 2>/dev/null | wc -l)
local my_role="${CURRENT_ROLE:-edge}"
local my_score
my_score=$(election_calculate_score "$my_did" "$my_telemetry" "$my_peer_count" "$my_role")
best_did="$my_did"
best_score="$my_score"
fi
# Check peer scores (if we have their telemetry)
while IFS= read -r peer_json; do
[ -z "$peer_json" ] && continue
local peer_did peer_role
peer_did=$(echo "$peer_json" | jsonfilter -e '@.did' 2>/dev/null)
peer_role=$(echo "$peer_json" | jsonfilter -e '@.role' 2>/dev/null || echo "edge")
[ -z "$peer_did" ] && continue
# Try to get peer's telemetry via API
local peer_addr peer_port
peer_addr=$(echo "$peer_json" | jsonfilter -e '@.address' 2>/dev/null)
peer_port=$(echo "$peer_json" | jsonfilter -e '@.port' 2>/dev/null || echo "7331")
local peer_telemetry
peer_telemetry=$(timeout 2 wget -qO- "http://$peer_addr:$peer_port/api/telemetry" 2>/dev/null)
if [ -n "$peer_telemetry" ]; then
# Count peer's peers (simplified: assume similar to ours)
local peer_peer_count="$my_peer_count"
local peer_score
peer_score=$(election_calculate_score "$peer_did" "$peer_telemetry" "$peer_peer_count" "$peer_role")
if [ "$peer_score" -gt "$best_score" ]; then
best_did="$peer_did"
best_score="$peer_score"
fi
fi
done < <(jsonfilter -i "$peers_file" -e '@[*]' 2>/dev/null)
# Save election result
echo "$best_did" > "$MESH_GATE_FILE"
# Save election state
cat > "$ELECTION_STATE" <<EOF
{
"mesh_gate": "$best_did",
"score": $best_score,
"elected_at": "$(date -Iseconds)",
"candidates_evaluated": $(jsonfilter -i "$peers_file" -e '@[*]' 2>/dev/null | wc -l)
}
EOF
# Log election result
if [ "$best_did" = "$my_did" ]; then
logger -t secubox-mesh "This node elected as mesh gate (score: $best_score)"
else
logger -t secubox-mesh "Mesh gate: $best_did (score: $best_score)"
fi
echo "$best_did"
}
# Get current mesh gate
election_get_gate() {
cat "$MESH_GATE_FILE" 2>/dev/null || echo ""
}
# Check if this node is the gate
election_is_gate() {
local current_gate
current_gate=$(election_get_gate)
local my_did="${NODE_DID:-$(cat /var/lib/mirrornet/identity/did.txt 2>/dev/null)}"
[ "$current_gate" = "$my_did" ]
}
# Force election (trigger immediate re-election)
election_force() {
rm -f "$MESH_GATE_FILE" "$ELECTION_STATE"
election_run
}
# Get election state
election_get_state() {
if [ -f "$ELECTION_STATE" ]; then
cat "$ELECTION_STATE"
else
echo '{"mesh_gate":"","score":0,"elected_at":"","candidates_evaluated":0}'
fi
}
# Initialize on source
election_init

View File

@ -0,0 +1,197 @@
#!/bin/sh
# SecuBox Mesh Telemetry Collection
# Collects system metrics for mesh sharing
# CyberMind — SecuBox — 2026
TELEMETRY_FILE="/var/lib/secubox-mesh/telemetry.json"
# Initialize telemetry
telemetry_init() {
mkdir -p "$(dirname "$TELEMETRY_FILE")"
}
# Get CPU usage percentage
_get_cpu_percent() {
local idle total
local cpu_line
cpu_line=$(head -1 /proc/stat)
set -- $cpu_line
shift # Remove "cpu" prefix
local user=$1 nice=$2 system=$3 idle_val=$4 iowait=$5
total=$((user + nice + system + idle_val + iowait))
idle=$idle_val
# Calculate percentage (non-idle)
if [ "$total" -gt 0 ]; then
echo $(( (total - idle) * 100 / total ))
else
echo 0
fi
}
# Get memory usage percentage
_get_memory_percent() {
local total free buffers cached available
local meminfo="/proc/meminfo"
total=$(grep "^MemTotal:" "$meminfo" | awk '{print $2}')
free=$(grep "^MemFree:" "$meminfo" | awk '{print $2}')
buffers=$(grep "^Buffers:" "$meminfo" | awk '{print $2}')
cached=$(grep "^Cached:" "$meminfo" | awk '{print $2}')
available=$(grep "^MemAvailable:" "$meminfo" | awk '{print $2}')
if [ -n "$available" ] && [ "$total" -gt 0 ]; then
echo $(( (total - available) * 100 / total ))
elif [ "$total" -gt 0 ]; then
local used=$((total - free - buffers - cached))
echo $(( used * 100 / total ))
else
echo 0
fi
}
# Get disk usage percentage
_get_disk_percent() {
local mount="${1:-/}"
df "$mount" 2>/dev/null | tail -1 | awk '{print int($5)}'
}
# Get load average
_get_load_avg() {
cut -d' ' -f1 /proc/loadavg
}
# Get uptime in seconds
_get_uptime() {
cut -d. -f1 /proc/uptime
}
# Get network interface stats
_get_network_stats() {
local interface="${1:-br-lan}"
local rx_bytes tx_bytes
rx_bytes=$(cat "/sys/class/net/$interface/statistics/rx_bytes" 2>/dev/null || echo 0)
tx_bytes=$(cat "/sys/class/net/$interface/statistics/tx_bytes" 2>/dev/null || echo 0)
echo "{\"rx_bytes\":$rx_bytes,\"tx_bytes\":$tx_bytes}"
}
# Get temperature (if available)
_get_temperature() {
local temp=""
# Try thermal zone
if [ -f /sys/class/thermal/thermal_zone0/temp ]; then
temp=$(cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null)
[ -n "$temp" ] && temp=$((temp / 1000))
fi
# Try hwmon
if [ -z "$temp" ] && [ -d /sys/class/hwmon ]; then
for hwmon in /sys/class/hwmon/hwmon*/temp1_input; do
[ -f "$hwmon" ] || continue
temp=$(cat "$hwmon" 2>/dev/null)
[ -n "$temp" ] && temp=$((temp / 1000))
break
done
fi
echo "${temp:-0}"
}
# Get WireGuard interface stats
_get_wireguard_stats() {
local interface="${1:-wg0}"
local peer_count=0
local rx_total=0
local tx_total=0
if command -v wg >/dev/null 2>&1; then
local stats
stats=$(wg show "$interface" transfer 2>/dev/null)
while read -r pubkey rx tx; do
[ -z "$pubkey" ] && continue
peer_count=$((peer_count + 1))
rx_total=$((rx_total + rx))
tx_total=$((tx_total + tx))
done <<EOF
$stats
EOF
fi
echo "{\"peers\":$peer_count,\"rx_bytes\":$rx_total,\"tx_bytes\":$tx_total}"
}
# Get connection count
_get_connection_count() {
local tcp udp
if [ -f /proc/net/tcp ]; then
tcp=$(( $(wc -l < /proc/net/tcp) - 1 ))
else
tcp=0
fi
if [ -f /proc/net/udp ]; then
udp=$(( $(wc -l < /proc/net/udp) - 1 ))
else
udp=0
fi
echo "{\"tcp\":$tcp,\"udp\":$udp}"
}
# Collect all telemetry
telemetry_collect() {
local timestamp cpu_percent memory_percent disk_percent
local load_avg uptime_seconds temperature
local network_stats wg_stats conn_stats
timestamp=$(date -Iseconds)
cpu_percent=$(_get_cpu_percent)
memory_percent=$(_get_memory_percent)
disk_percent=$(_get_disk_percent)
load_avg=$(_get_load_avg)
uptime_seconds=$(_get_uptime)
temperature=$(_get_temperature)
network_stats=$(_get_network_stats)
wg_stats=$(_get_wireguard_stats)
conn_stats=$(_get_connection_count)
cat <<EOF
{
"timestamp": "$timestamp",
"cpu_percent": $cpu_percent,
"memory_percent": $memory_percent,
"disk_percent": $disk_percent,
"load_avg": $load_avg,
"uptime": $uptime_seconds,
"temperature": $temperature,
"network": $network_stats,
"wireguard": $wg_stats,
"connections": $conn_stats
}
EOF
}
# Store telemetry
telemetry_store() {
telemetry_collect > "$TELEMETRY_FILE"
}
# Get latest telemetry
telemetry_get() {
if [ -f "$TELEMETRY_FILE" ]; then
cat "$TELEMETRY_FILE"
else
telemetry_collect
fi
}
# Initialize on source
telemetry_init

View File

@ -0,0 +1,208 @@
#!/bin/sh
# SecuBox Mesh Topology Management
# Tracks mesh nodes, edges, and generates topology graph
# CyberMind — SecuBox — 2026
TOPO_FILE="/var/lib/secubox-mesh/topology.json"
NODES_FILE="/var/lib/secubox-mesh/nodes.json"
EDGES_FILE="/var/lib/secubox-mesh/edges.json"
# Initialize topology storage
topology_init() {
mkdir -p "$(dirname "$TOPO_FILE")"
[ -f "$TOPO_FILE" ] || echo '{"nodes":[],"edges":[],"mesh_gate":""}' > "$TOPO_FILE"
[ -f "$NODES_FILE" ] || echo '[]' > "$NODES_FILE"
[ -f "$EDGES_FILE" ] || echo '[]' > "$EDGES_FILE"
}
# Add or update a node in topology
topology_add_node() {
local did="$1"
local address="$2"
local role="${3:-edge}"
local last_seen
last_seen=$(date -Iseconds)
# Check if node exists
local exists
exists=$(jsonfilter -i "$NODES_FILE" -e "@[\"$did\"]" 2>/dev/null)
local tmp_file="/tmp/topo_nodes_$$.json"
if [ -n "$exists" ]; then
# Update existing node
local updated='{"did":"'"$did"'","address":"'"$address"'","role":"'"$role"'","last_seen":"'"$last_seen"'","zkp_valid":true}'
# Use sed to update (simplified)
sed "s|{\"did\":\"$did\"[^}]*}|$updated|" "$NODES_FILE" > "$tmp_file"
else
# Add new node
if [ "$(cat "$NODES_FILE")" = "[]" ]; then
echo '[{"did":"'"$did"'","address":"'"$address"'","role":"'"$role"'","last_seen":"'"$last_seen"'","zkp_valid":true}]' > "$tmp_file"
else
# Insert before closing bracket
sed "s/\]$/,{\"did\":\"$did\",\"address\":\"$address\",\"role\":\"$role\",\"last_seen\":\"$last_seen\",\"zkp_valid\":true}]/" "$NODES_FILE" > "$tmp_file"
fi
fi
mv "$tmp_file" "$NODES_FILE"
}
# Remove a node from topology
topology_remove_node() {
local did="$1"
local tmp_file="/tmp/topo_nodes_$$.json"
# Remove node entry (simplified - removes entire object)
grep -v "\"did\":\"$did\"" "$NODES_FILE" > "$tmp_file" || echo '[]' > "$tmp_file"
mv "$tmp_file" "$NODES_FILE"
# Also remove edges involving this node
topology_remove_edges_for_node "$did"
}
# Add an edge between two nodes
topology_add_edge() {
local from_did="$1"
local to_did="$2"
local weight="${3:-1}"
local active="${4:-true}"
local edge_id="${from_did}:${to_did}"
local tmp_file="/tmp/topo_edges_$$.json"
if [ "$(cat "$EDGES_FILE")" = "[]" ]; then
echo '[{"id":"'"$edge_id"'","from":"'"$from_did"'","to":"'"$to_did"'","weight":'"$weight"',"active":'"$active"'}]' > "$tmp_file"
else
# Check if edge exists
if grep -q "\"id\":\"$edge_id\"" "$EDGES_FILE"; then
return 0 # Edge already exists
fi
sed "s/\]$/,{\"id\":\"$edge_id\",\"from\":\"$from_did\",\"to\":\"$to_did\",\"weight\":$weight,\"active\":$active}]/" "$EDGES_FILE" > "$tmp_file"
fi
mv "$tmp_file" "$EDGES_FILE"
}
# Remove edges for a node
topology_remove_edges_for_node() {
local did="$1"
local tmp_file="/tmp/topo_edges_$$.json"
grep -v "\"$did\"" "$EDGES_FILE" > "$tmp_file" || echo '[]' > "$tmp_file"
mv "$tmp_file" "$EDGES_FILE"
}
# Get all nodes
topology_get_nodes() {
cat "$NODES_FILE" 2>/dev/null || echo '[]'
}
# Get all edges
topology_get_edges() {
cat "$EDGES_FILE" 2>/dev/null || echo '[]'
}
# Get full topology
topology_get() {
local nodes edges mesh_gate
nodes=$(cat "$NODES_FILE" 2>/dev/null || echo '[]')
edges=$(cat "$EDGES_FILE" 2>/dev/null || echo '[]')
mesh_gate=$(cat /var/lib/secubox-mesh/mesh_gate 2>/dev/null || echo "")
cat <<EOF
{"nodes":$nodes,"edges":$edges,"mesh_gate":"$mesh_gate"}
EOF
}
# Update topology from peer discovery
topology_update() {
local peers_file="/var/lib/secubox-mesh/peers.json"
[ -f "$peers_file" ] || return
# Add this node first
local my_did my_addr my_role
my_did="${NODE_DID:-$(cat /var/lib/mirrornet/identity/did.txt 2>/dev/null)}"
my_addr=$(uci -q get network.lan.ipaddr || echo "0.0.0.0")
my_role="${CURRENT_ROLE:-edge}"
[ -n "$my_did" ] && topology_add_node "$my_did" "$my_addr" "$my_role"
# Process discovered peers
local peer_count=0
while IFS= read -r peer_json; do
[ -z "$peer_json" ] && continue
local peer_did peer_addr peer_role
peer_did=$(echo "$peer_json" | jsonfilter -e '@.did' 2>/dev/null)
peer_addr=$(echo "$peer_json" | jsonfilter -e '@.address' 2>/dev/null)
peer_role=$(echo "$peer_json" | jsonfilter -e '@.role' 2>/dev/null || echo "edge")
[ -z "$peer_did" ] && continue
topology_add_node "$peer_did" "$peer_addr" "$peer_role"
# Add edge from this node to peer
[ -n "$my_did" ] && topology_add_edge "$my_did" "$peer_did" 1 true
peer_count=$((peer_count + 1))
done < <(jsonfilter -i "$peers_file" -e '@[*]' 2>/dev/null)
# Save combined topology
topology_get > "$TOPO_FILE"
logger -t secubox-mesh "Topology updated: $peer_count peers"
}
# Prune stale nodes (not seen in 5 minutes)
topology_prune_stale() {
local stale_threshold=300 # 5 minutes
local current_time
current_time=$(date +%s)
local tmp_file="/tmp/topo_nodes_prune_$$.json"
local pruned=0
echo '[' > "$tmp_file"
local first=1
while IFS= read -r node_json; do
[ -z "$node_json" ] && continue
local last_seen_str last_seen_ts age
last_seen_str=$(echo "$node_json" | jsonfilter -e '@.last_seen' 2>/dev/null)
# Convert ISO date to timestamp (simplified)
last_seen_ts=$(date -d "$last_seen_str" +%s 2>/dev/null || echo "$current_time")
age=$((current_time - last_seen_ts))
if [ "$age" -lt "$stale_threshold" ]; then
[ "$first" = "1" ] || echo ',' >> "$tmp_file"
echo "$node_json" >> "$tmp_file"
first=0
else
pruned=$((pruned + 1))
local did
did=$(echo "$node_json" | jsonfilter -e '@.did' 2>/dev/null)
logger -t secubox-mesh "Pruned stale node: $did (age: ${age}s)"
fi
done < <(jsonfilter -i "$NODES_FILE" -e '@[*]' 2>/dev/null)
echo ']' >> "$tmp_file"
mv "$tmp_file" "$NODES_FILE"
[ "$pruned" -gt 0 ] && topology_update
}
# Get node count
topology_node_count() {
jsonfilter -i "$NODES_FILE" -e '@[*]' 2>/dev/null | wc -l
}
# Get edge count
topology_edge_count() {
jsonfilter -i "$EDGES_FILE" -e '@[*]' 2>/dev/null | wc -l
}
# Initialize on source
topology_init

View File

@ -0,0 +1,150 @@
#!/bin/sh
# SecuBox Mesh RPCD Handler
# Exposes mesh daemon commands via ubus
# CyberMind — SecuBox — 2026
. /lib/functions.sh
. /usr/share/libubox/jshn.sh
SOCKET="/var/run/secuboxd/topo.sock"
# Send command to daemon
_send_cmd() {
local cmd="$1"
if [ ! -S "$SOCKET" ]; then
echo '{"error":"Daemon not running"}'
return 1
fi
if command -v socat >/dev/null 2>&1; then
echo "$cmd" | socat - "UNIX-CONNECT:$SOCKET" 2>/dev/null
else
echo '{"error":"socat not available"}'
return 1
fi
}
# List available methods
case_list() {
cat <<'EOF'
{
"status": {},
"peers": {},
"topology": {},
"nodes": {},
"node_info": {},
"node_rotate": {},
"telemetry": {},
"ping": {},
"get_config": {},
"set_config": { "role": "string", "beacon_interval": "number" },
"restart": {}
}
EOF
}
# Call method
case_call() {
local method="$1"
case "$method" in
status)
_send_cmd "mesh.status"
;;
peers)
_send_cmd "mesh.peers"
;;
topology)
_send_cmd "mesh.topology"
;;
nodes)
_send_cmd "mesh.nodes"
;;
node_info)
_send_cmd "node.info"
;;
node_rotate)
_send_cmd "node.rotate"
;;
telemetry)
_send_cmd "telemetry.latest"
;;
ping)
local response
response=$(_send_cmd "ping")
if echo "$response" | grep -q '"pong":true'; then
json_init
json_add_boolean running 1
json_dump
else
json_init
json_add_boolean running 0
json_add_string error "Daemon not responding"
json_dump
fi
;;
get_config)
json_init
config_load secubox
local enabled role subnet beacon_interval
config_get_bool enabled mesh enabled 1
config_get role mesh role "edge"
config_get subnet mesh subnet "10.42.0.0/16"
config_get beacon_interval mesh beacon_interval "30"
json_add_boolean enabled "$enabled"
json_add_string role "$role"
json_add_string subnet "$subnet"
json_add_int beacon_interval "$beacon_interval"
json_dump
;;
set_config)
read -r input
local role beacon_interval
json_load "$input"
json_get_var role role
json_get_var beacon_interval beacon_interval
[ -n "$role" ] && uci set secubox.mesh.role="$role"
[ -n "$beacon_interval" ] && uci set secubox.mesh.beacon_interval="$beacon_interval"
uci commit secubox
json_init
json_add_boolean success 1
json_dump
;;
restart)
/etc/init.d/secuboxd restart >/dev/null 2>&1
json_init
json_add_boolean success 1
json_dump
;;
*)
json_init
json_add_string error "Unknown method: $method"
json_dump
return 1
;;
esac
}
# Main dispatcher
case "$1" in
list)
case_list
;;
call)
case_call "$2"
;;
*)
echo '{"error":"Invalid command"}'
exit 1
;;
esac

View File

@ -0,0 +1,285 @@
#!/bin/sh
# SecuBox Mesh Control CLI (secuboxctl)
# Communicates with secuboxd via Unix control socket
# CyberMind — SecuBox — 2026
SOCKET="/var/run/secuboxd/topo.sock"
VERSION="1.0.0"
# Send command to daemon and get response
send_command() {
local cmd="$1"
if [ ! -S "$SOCKET" ]; then
echo '{"error":"Daemon not running","socket":"'"$SOCKET"'"}'
return 1
fi
# Use socat if available
if command -v socat >/dev/null 2>&1; then
echo "$cmd" | socat - UNIX-CONNECT:"$SOCKET"
else
# Fallback: use FIFO approach
local fifo_in="/var/run/secuboxd/socket_in"
local fifo_out="/var/run/secuboxd/socket_out"
if [ -p "$fifo_in" ] && [ -p "$fifo_out" ]; then
echo "$cmd" > "$fifo_in"
timeout 5 cat "$fifo_out" 2>/dev/null || echo '{"error":"Timeout"}'
else
echo '{"error":"Socket communication not available"}'
return 1
fi
fi
}
# Format JSON output for display
format_output() {
local json="$1"
local format="${2:-json}"
case "$format" in
json)
if command -v jq >/dev/null 2>&1; then
echo "$json" | jq .
else
echo "$json"
fi
;;
table)
# Simple table formatting using jsonfilter
echo "$json"
;;
*)
echo "$json"
;;
esac
}
# Commands
cmd_status() {
local response
response=$(send_command "mesh.status")
format_output "$response" "$1"
}
cmd_peers() {
local response
response=$(send_command "mesh.peers")
if [ "$1" = "-q" ] || [ "$1" = "--quiet" ]; then
echo "$response" | jsonfilter -e '@[*].did' 2>/dev/null | wc -l
else
format_output "$response" "$1"
fi
}
cmd_topology() {
local response
response=$(send_command "mesh.topology")
format_output "$response" "$1"
}
cmd_nodes() {
local response
response=$(send_command "mesh.nodes")
format_output "$response" "$1"
}
cmd_info() {
local response
response=$(send_command "node.info")
format_output "$response" "$1"
}
cmd_rotate() {
local response
response=$(send_command "node.rotate")
local success
success=$(echo "$response" | jsonfilter -e '@.success' 2>/dev/null)
if [ "$success" = "true" ]; then
echo "Key rotation successful"
echo "$response" | jsonfilter -e '@.new_expiry' 2>/dev/null
return 0
else
echo "Key rotation failed"
echo "$response" | jsonfilter -e '@.error' 2>/dev/null
return 1
fi
}
cmd_telemetry() {
local response
response=$(send_command "telemetry.latest")
format_output "$response" "$1"
}
cmd_ping() {
local response
response=$(send_command "ping")
if echo "$response" | grep -q '"pong":true'; then
echo "Daemon is running"
return 0
else
echo "Daemon not responding"
return 1
fi
}
# Service management
cmd_start() {
if pgrep -x secuboxd >/dev/null 2>&1; then
echo "secuboxd is already running"
return 1
fi
/etc/init.d/secuboxd start
sleep 1
if pgrep -x secuboxd >/dev/null 2>&1; then
echo "secuboxd started"
return 0
else
echo "Failed to start secuboxd"
return 1
fi
}
cmd_stop() {
/etc/init.d/secuboxd stop
sleep 1
if pgrep -x secuboxd >/dev/null 2>&1; then
echo "secuboxd still running, killing..."
pkill -9 secuboxd
fi
echo "secuboxd stopped"
}
cmd_restart() {
cmd_stop
sleep 1
cmd_start
}
# Show usage
usage() {
cat <<EOF
SecuBox Mesh Control CLI (secuboxctl) v$VERSION
Usage: secuboxctl <command> [options]
Mesh Commands:
mesh status Show mesh status (state, role, peers, gate)
mesh peers List connected peers
mesh topology Get mesh topology graph
mesh nodes List all known nodes
Node Commands:
node info Show this node's information
node rotate Rotate node keys
Telemetry:
telemetry latest Get latest system telemetry
Service Commands:
start Start secuboxd daemon
stop Stop secuboxd daemon
restart Restart secuboxd daemon
ping Check if daemon is running
Options:
-f, --format Output format: json (default), table
-q, --quiet Minimal output
-h, --help Show this help
-v, --version Show version
Examples:
secuboxctl mesh status
secuboxctl mesh peers -f table
secuboxctl node info
secuboxctl telemetry latest
Socket: $SOCKET
EOF
}
# Main
main() {
local cmd="$1"
shift
case "$cmd" in
mesh)
local subcmd="$1"
shift
case "$subcmd" in
status) cmd_status "$@" ;;
peers) cmd_peers "$@" ;;
topology) cmd_topology "$@" ;;
nodes) cmd_nodes "$@" ;;
*) echo "Unknown mesh command: $subcmd"; usage; exit 1 ;;
esac
;;
node)
local subcmd="$1"
shift
case "$subcmd" in
info) cmd_info "$@" ;;
rotate) cmd_rotate "$@" ;;
*) echo "Unknown node command: $subcmd"; usage; exit 1 ;;
esac
;;
telemetry)
local subcmd="$1"
shift
case "$subcmd" in
latest) cmd_telemetry "$@" ;;
*) echo "Unknown telemetry command: $subcmd"; usage; exit 1 ;;
esac
;;
status)
cmd_status "$@"
;;
peers)
cmd_peers "$@"
;;
topology)
cmd_topology "$@"
;;
start)
cmd_start
;;
stop)
cmd_stop
;;
restart)
cmd_restart
;;
ping)
cmd_ping
;;
-h|--help|help)
usage
exit 0
;;
-v|--version|version)
echo "secuboxctl $VERSION (OpenWrt)"
exit 0
;;
"")
usage
exit 0
;;
*)
echo "Unknown command: $cmd"
usage
exit 1
;;
esac
}
main "$@"

View File

@ -0,0 +1,412 @@
#!/bin/sh
# SecuBox Mesh Daemon (secuboxd) for OpenWrt
# Unix control socket server with mesh topology management
# CyberMind — SecuBox — 2026
. /lib/functions.sh
# Paths
RUNDIR="/var/run/secuboxd"
SOCKET="$RUNDIR/topo.sock"
PIDFILE="$RUNDIR/secuboxd.pid"
STATEDIR="/var/lib/secubox-mesh"
LOGFILE="/var/log/secuboxd.log"
# Source mesh libraries
. /usr/lib/secubox-mesh/topology.sh
. /usr/lib/secubox-mesh/discovery.sh
. /usr/lib/secubox-mesh/telemetry.sh
. /usr/lib/secubox-mesh/control.sh
. /usr/lib/secubox-mesh/election.sh
# Source mirrornet libraries if available
[ -f /usr/lib/mirrornet/identity.sh ] && . /usr/lib/mirrornet/identity.sh
[ -f /usr/lib/mirrornet/gossip.sh ] && . /usr/lib/mirrornet/gossip.sh
[ -f /usr/lib/mirrornet/health.sh ] && . /usr/lib/mirrornet/health.sh
# Global state
DAEMON_START_TIME=0
MESH_STATE="starting"
CURRENT_ROLE="edge"
MESH_GATE=""
PEER_COUNT=0
log() {
local level="$1"
shift
local msg="$*"
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $msg" >> "$LOGFILE"
logger -t secuboxd -p "daemon.$level" "$msg"
}
log_info() { log "info" "$@"; }
log_warn() { log "warn" "$@"; }
log_error() { log "error" "$@"; }
# Save daemon state for subprocess access
save_daemon_state() {
cat > "$STATEDIR/daemon_state" <<STATE_EOF
NODE_DID="$NODE_DID"
CURRENT_ROLE="$CURRENT_ROLE"
MESH_STATE="$MESH_STATE"
MESH_GATE="$MESH_GATE"
PEER_COUNT="$PEER_COUNT"
DAEMON_START_TIME="$DAEMON_START_TIME"
BEACON_INTERVAL="$BEACON_INTERVAL"
STATEDIR="$STATEDIR"
STATE_EOF
}
# Initialize daemon
daemon_init() {
log_info "Initializing SecuBox mesh daemon"
# Create directories
mkdir -p "$RUNDIR" "$STATEDIR"
chmod 755 "$RUNDIR"
# Initialize state files
echo '{"nodes":[],"edges":[],"mesh_gate":""}' > "$STATEDIR/topology.json"
echo '[]' > "$STATEDIR/peers.json"
echo '{}' > "$STATEDIR/telemetry.json"
# Get node identity
if type identity_get_did >/dev/null 2>&1; then
NODE_DID=$(identity_get_did)
else
# Fallback DID generation
local machine_id mac_addr
machine_id=$(cat /etc/machine-id 2>/dev/null || echo "openwrt")
mac_addr=$(cat /sys/class/net/br-lan/address 2>/dev/null || \
cat /sys/class/net/eth0/address 2>/dev/null || echo "00:00:00:00:00:00")
NODE_DID="did:plc:$(echo -n "${machine_id}:${mac_addr}" | md5sum | cut -c1-16)"
fi
# Load configuration
config_load secubox
config_get CURRENT_ROLE mesh role "edge"
config_get BEACON_INTERVAL mesh beacon_interval "30"
config_get ELECTION_INTERVAL mesh election_interval "60"
config_get MDNS_SERVICE mesh mdns_service "_secubox._udp"
DAEMON_START_TIME=$(date +%s)
# Save state for socket handler subprocess
save_daemon_state
log_info "Node DID: $NODE_DID"
log_info "Role: $CURRENT_ROLE"
}
# Handle control socket command
handle_command() {
local cmd="$1"
local response=""
case "$cmd" in
"ping")
response='{"pong":true}'
;;
"mesh.status")
response=$(cmd_mesh_status)
;;
"mesh.peers")
response=$(cmd_mesh_peers)
;;
"mesh.topology")
response=$(cmd_mesh_topology)
;;
"mesh.nodes")
response=$(cmd_mesh_nodes)
;;
"node.info")
response=$(cmd_node_info)
;;
"node.rotate")
response=$(cmd_node_rotate)
;;
"telemetry.latest")
response=$(cmd_telemetry_latest)
;;
*)
response='{"error":"Unknown command","command":"'"$cmd"'"}'
;;
esac
echo "$response"
}
# Command: mesh.status
cmd_mesh_status() {
local uptime=$(($(date +%s) - DAEMON_START_TIME))
cat <<EOF
{"state":"$MESH_STATE","peer_count":$PEER_COUNT,"role":"$CURRENT_ROLE","mesh_gate":"$MESH_GATE","uptime":$uptime,"did":"$NODE_DID"}
EOF
}
# Command: mesh.peers
cmd_mesh_peers() {
if [ -f "$STATEDIR/peers.json" ]; then
cat "$STATEDIR/peers.json"
else
echo '[]'
fi
}
# Command: mesh.topology
cmd_mesh_topology() {
if [ -f "$STATEDIR/topology.json" ]; then
cat "$STATEDIR/topology.json"
else
echo '{"nodes":[],"edges":[],"mesh_gate":""}'
fi
}
# Command: mesh.nodes
cmd_mesh_nodes() {
topology_get_nodes
}
# Command: node.info
cmd_node_info() {
local zkp_valid="false"
[ -f "$STATEDIR/zkp_valid" ] && zkp_valid="true"
local pubkey=""
if [ -f /var/lib/mirrornet/identity/keys/primary.pub ]; then
pubkey=$(cat /var/lib/mirrornet/identity/keys/primary.pub 2>/dev/null | tr -d '\n')
fi
cat <<EOF
{"did":"$NODE_DID","role":"$CURRENT_ROLE","public_key":"$pubkey","zkp_valid":$zkp_valid,"mesh_gate":"$MESH_GATE"}
EOF
}
# Command: node.rotate
cmd_node_rotate() {
if type identity_rotate_key >/dev/null 2>&1; then
identity_rotate_key "primary"
local new_expiry
new_expiry=$(date -d "+30 days" -Iseconds 2>/dev/null || date -Iseconds)
echo '{"success":true,"new_expiry":"'"$new_expiry"'"}'
else
echo '{"success":false,"error":"Identity rotation not available"}'
fi
}
# Command: telemetry.latest
cmd_telemetry_latest() {
telemetry_collect
}
# Control socket server using netcat
start_socket_server() {
log_info "Starting control socket server at $SOCKET"
# Remove old socket
rm -f "$SOCKET"
# Create socket directory
mkdir -p "$(dirname "$SOCKET")"
# Use socat if available, fallback to nc
if command -v socat >/dev/null 2>&1; then
socat UNIX-LISTEN:"$SOCKET",fork EXEC:"/usr/sbin/secuboxd --handle-client" &
SOCKET_PID=$!
else
# Fallback: use a FIFO-based approach
local fifo_in="$RUNDIR/socket_in"
local fifo_out="$RUNDIR/socket_out"
mkfifo "$fifo_in" "$fifo_out" 2>/dev/null
while true; do
if read -r cmd < "$fifo_in" 2>/dev/null; then
handle_command "$cmd" > "$fifo_out"
fi
sleep 0.1
done &
SOCKET_PID=$!
fi
log_info "Socket server started (PID: $SOCKET_PID)"
}
# Handle client connection (for socat exec mode)
handle_client() {
# Load daemon state for subprocess
STATEDIR="/var/lib/secubox-mesh"
[ -f "$STATEDIR/daemon_state" ] && . "$STATEDIR/daemon_state"
while IFS= read -r line; do
[ -z "$line" ] && continue
handle_command "$line"
done
}
# mDNS service advertisement
start_mdns() {
log_info "Starting mDNS advertisement"
local wg_port
wg_port=$(uci -q get network.wg0.listen_port || echo "51820")
# Use umdns if available
if command -v umdns >/dev/null 2>&1; then
# Create service file for umdns
mkdir -p /var/run/umdns
cat > /var/run/umdns/secubox.json <<EOF
{
"secubox": {
"service": "_secubox._udp.local",
"port": $wg_port,
"txt": [
"did=$NODE_DID",
"role=$CURRENT_ROLE",
"version=1.0.0"
]
}
}
EOF
# Reload umdns
/etc/init.d/umdns reload 2>/dev/null
log_info "mDNS service registered via umdns"
elif command -v avahi-publish >/dev/null 2>&1; then
# Fallback to avahi-publish
avahi-publish -s "secubox-${NODE_DID##*:}" "_secubox._udp" "$wg_port" \
"did=$NODE_DID" "role=$CURRENT_ROLE" "version=1.0.0" &
AVAHI_PID=$!
log_info "mDNS service registered via avahi-publish (PID: $AVAHI_PID)"
else
log_warn "No mDNS daemon available (umdns or avahi)"
fi
}
# Stop mDNS advertisement
stop_mdns() {
rm -f /var/run/umdns/secubox.json
/etc/init.d/umdns reload 2>/dev/null
[ -n "$AVAHI_PID" ] && kill "$AVAHI_PID" 2>/dev/null
}
# Main daemon loop
daemon_loop() {
local loop_count=0
MESH_STATE="running"
save_daemon_state
log_info "Entering main daemon loop"
while true; do
loop_count=$((loop_count + 1))
# Peer discovery (every beacon interval)
if [ $((loop_count % BEACON_INTERVAL)) -eq 0 ]; then
discovery_scan_peers
PEER_COUNT=$(discovery_get_peer_count)
log_info "Peer discovery: found $PEER_COUNT peers"
save_daemon_state
fi
# Topology update (every 2x beacon interval)
if [ $((loop_count % (BEACON_INTERVAL * 2))) -eq 0 ]; then
topology_update
fi
# Gate election (every election interval)
if [ $((loop_count % ELECTION_INTERVAL)) -eq 0 ]; then
local old_gate="$MESH_GATE"
MESH_GATE=$(election_run)
if [ "$MESH_GATE" != "$old_gate" ]; then
log_info "Mesh gate changed: $old_gate -> $MESH_GATE"
fi
# Update state file with new values
save_daemon_state
fi
# Telemetry collection (every 60 seconds)
if [ $((loop_count % 60)) -eq 0 ]; then
telemetry_collect > "$STATEDIR/telemetry.json"
fi
# Health check (every 30 seconds)
if [ $((loop_count % 30)) -eq 0 ] && type health_check >/dev/null 2>&1; then
health_check
fi
sleep 1
done
}
# Signal handlers
cleanup() {
log_info "Shutting down secuboxd"
MESH_STATE="stopping"
stop_mdns
[ -n "$SOCKET_PID" ] && kill "$SOCKET_PID" 2>/dev/null
rm -f "$SOCKET" "$PIDFILE"
log_info "SecuBox mesh daemon stopped"
exit 0
}
trap cleanup INT TERM
# Main entry point
main() {
case "$1" in
--handle-client)
handle_client
exit 0
;;
--foreground|-f)
daemon_init
start_socket_server
start_mdns
daemon_loop
;;
--version|-v)
echo "secuboxd 1.0.0 (OpenWrt)"
exit 0
;;
--help|-h)
cat <<EOF
SecuBox Mesh Daemon (secuboxd)
Usage: secuboxd [OPTION]
Options:
-f, --foreground Run in foreground (for procd)
-v, --version Show version
-h, --help Show this help
Control socket: $SOCKET
Configuration: /etc/config/secubox
Commands (via socket):
ping - Check daemon is alive
mesh.status - Get mesh status
mesh.peers - List connected peers
mesh.topology - Get mesh topology
mesh.nodes - List all nodes
node.info - Get this node's info
node.rotate - Rotate node keys
telemetry.latest - Get latest telemetry
EOF
exit 0
;;
*)
# Default: run in foreground for procd
daemon_init
echo $$ > "$PIDFILE"
start_socket_server
start_mdns
daemon_loop
;;
esac
}
main "$@"

View File

@ -0,0 +1,30 @@
{
"luci-app-secubox-mesh": {
"description": "Grant access to SecuBox Mesh Daemon",
"read": {
"ubus": {
"luci.secubox-mesh": [
"status",
"peers",
"topology",
"nodes",
"node_info",
"telemetry",
"ping",
"get_config"
]
},
"uci": ["secubox"]
},
"write": {
"ubus": {
"luci.secubox-mesh": [
"node_rotate",
"set_config",
"restart"
]
},
"uci": ["secubox"]
}
}
}