diff --git a/.claude/HISTORY.md b/.claude/HISTORY.md index 98798060..00f559d9 100644 --- a/.claude/HISTORY.md +++ b/.claude/HISTORY.md @@ -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 diff --git a/.claude/WIP.md b/.claude/WIP.md index 6e26bfd5..259fab71 100644 --- a/.claude/WIP.md +++ b/.claude/WIP.md @@ -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)** diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-mesh_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-mesh_1.0.0-r1_all.ipk new file mode 100644 index 00000000..0c1bedcd Binary files /dev/null and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-mesh_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-mesh/Makefile b/package/secubox/secubox-mesh/Makefile new file mode 100644 index 00000000..8b19a966 --- /dev/null +++ b/package/secubox/secubox-mesh/Makefile @@ -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 +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)) diff --git a/package/secubox/secubox-mesh/files/etc/config/secubox b/package/secubox/secubox-mesh/files/etc/config/secubox new file mode 100644 index 00000000..08a46dd7 --- /dev/null +++ b/package/secubox/secubox-mesh/files/etc/config/secubox @@ -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' diff --git a/package/secubox/secubox-mesh/files/etc/init.d/secuboxd b/package/secubox/secubox-mesh/files/etc/init.d/secuboxd new file mode 100755 index 00000000..cf1b1a16 --- /dev/null +++ b/package/secubox/secubox-mesh/files/etc/init.d/secuboxd @@ -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 +} diff --git a/package/secubox/secubox-mesh/files/usr/lib/secubox-mesh/control.sh b/package/secubox/secubox-mesh/files/usr/lib/secubox-mesh/control.sh new file mode 100755 index 00000000..a2a14b05 --- /dev/null +++ b/package/secubox/secubox-mesh/files/usr/lib/secubox-mesh/control.sh @@ -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 < "$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 </dev/null) + + if echo "$response" | grep -q '"pong":true'; then + return 0 + fi + + return 1 +} diff --git a/package/secubox/secubox-mesh/files/usr/lib/secubox-mesh/discovery.sh b/package/secubox/secubox-mesh/files/usr/lib/secubox-mesh/discovery.sh new file mode 100755 index 00000000..9bf90215 --- /dev/null +++ b/package/secubox/secubox-mesh/files/usr/lib/secubox-mesh/discovery.sh @@ -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 diff --git a/package/secubox/secubox-mesh/files/usr/lib/secubox-mesh/election.sh b/package/secubox/secubox-mesh/files/usr/lib/secubox-mesh/election.sh new file mode 100755 index 00000000..5b2045a7 --- /dev/null +++ b/package/secubox/secubox-mesh/files/usr/lib/secubox-mesh/election.sh @@ -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" </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 diff --git a/package/secubox/secubox-mesh/files/usr/lib/secubox-mesh/telemetry.sh b/package/secubox/secubox-mesh/files/usr/lib/secubox-mesh/telemetry.sh new file mode 100755 index 00000000..64850f1c --- /dev/null +++ b/package/secubox/secubox-mesh/files/usr/lib/secubox-mesh/telemetry.sh @@ -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 < "$TELEMETRY_FILE" +} + +# Get latest telemetry +telemetry_get() { + if [ -f "$TELEMETRY_FILE" ]; then + cat "$TELEMETRY_FILE" + else + telemetry_collect + fi +} + +# Initialize on source +telemetry_init diff --git a/package/secubox/secubox-mesh/files/usr/lib/secubox-mesh/topology.sh b/package/secubox/secubox-mesh/files/usr/lib/secubox-mesh/topology.sh new file mode 100755 index 00000000..cdba68c6 --- /dev/null +++ b/package/secubox/secubox-mesh/files/usr/lib/secubox-mesh/topology.sh @@ -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 </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 diff --git a/package/secubox/secubox-mesh/files/usr/libexec/rpcd/luci.secubox-mesh b/package/secubox/secubox-mesh/files/usr/libexec/rpcd/luci.secubox-mesh new file mode 100755 index 00000000..aa5131cb --- /dev/null +++ b/package/secubox/secubox-mesh/files/usr/libexec/rpcd/luci.secubox-mesh @@ -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 diff --git a/package/secubox/secubox-mesh/files/usr/sbin/secuboxctl b/package/secubox/secubox-mesh/files/usr/sbin/secuboxctl new file mode 100755 index 00000000..1034ab17 --- /dev/null +++ b/package/secubox/secubox-mesh/files/usr/sbin/secuboxctl @@ -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 < [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 "$@" diff --git a/package/secubox/secubox-mesh/files/usr/sbin/secuboxd b/package/secubox/secubox-mesh/files/usr/sbin/secuboxd new file mode 100755 index 00000000..49191225 --- /dev/null +++ b/package/secubox/secubox-mesh/files/usr/sbin/secuboxd @@ -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" < "$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 </dev/null | tr -d '\n') + fi + + cat </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 </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 < "$PIDFILE" + start_socket_server + start_mdns + daemon_loop + ;; + esac +} + +main "$@" diff --git a/package/secubox/secubox-mesh/root/usr/share/rpcd/acl.d/luci-app-secubox-mesh.json b/package/secubox/secubox-mesh/root/usr/share/rpcd/acl.d/luci-app-secubox-mesh.json new file mode 100644 index 00000000..eb911e23 --- /dev/null +++ b/package/secubox/secubox-mesh/root/usr/share/rpcd/acl.d/luci-app-secubox-mesh.json @@ -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"] + } + } +}