diff --git a/.claude/WIP.md b/.claude/WIP.md index 05ecd1a8..3c291cfa 100644 --- a/.claude/WIP.md +++ b/.claude/WIP.md @@ -64,6 +64,25 @@ _Last updated: 2026-02-20 (v0.24.0 - Matrix + SaaS Relay + Media Hub)_ ### Just Completed (2026-02-20) +- **LuCI VM Manager** — DONE (2026-02-20) + - `luci-app-vm` package for LXC container management dashboard + - Status bar: total/running/stopped containers, disk usage + - Container cards with Start/Stop/Restart, Snapshot, Export controls + - RPCD handler with 10 methods: status, list, info, logs, start, stop, restart, snapshot, export + - Polling for live status updates + +- **Vortex Firewall Stats Fix** — DONE (2026-02-20) + - Enabled BIND RPZ logging for blocked queries + - Created `/usr/sbin/vortex-firewall-stats` script to parse logs + - Fixed RPCD handler to read hit_count from stats file + - Added cron job for automatic stats updates every 5 minutes + - Verified: 12,370 domains blocked, RPZ NXDOMAIN working + +- **SaaS Relay HAProxy Integration** — DONE (2026-02-20) + - Fixed relay.gk2.secubox.in routing to mitmproxy on port 8891 + - Created SaaS relay dashboard HTML at /srv/saas-relay/web/ + - HexoJS fallback via uhttpd on port 4000 + - **Matrix Homeserver (Conduit)** — DONE (2026-02-20) - E2EE mesh messaging server using Conduit Matrix homeserver - LXC container with pre-built ARM64 Conduit binary (0.10.12) diff --git a/package/secubox/luci-app-vm/Makefile b/package/secubox/luci-app-vm/Makefile new file mode 100644 index 00000000..9572d7a5 --- /dev/null +++ b/package/secubox/luci-app-vm/Makefile @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# SecuBox VM Management LuCI Interface + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI VM Management Dashboard +LUCI_DESCRIPTION:=Manage LXC containers and virtual machine images +LUCI_DEPENDS:=+lxc +luci-base +LUCI_PKGARCH:=all + +PKG_NAME:=luci-app-vm +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 +PKG_MAINTAINER:=SecuBox +PKG_LICENSE:=GPL-3.0-or-later + +include $(TOPDIR)/feeds/luci/luci.mk + +define Package/luci-app-vm/install + $(INSTALL_DIR) $(1)/usr/libexec/rpcd + $(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.vm $(1)/usr/libexec/rpcd/ + $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d + $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-vm.json $(1)/usr/share/rpcd/acl.d/ + $(INSTALL_DIR) $(1)/usr/share/luci/menu.d + $(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-vm.json $(1)/usr/share/luci/menu.d/ + $(INSTALL_DIR) $(1)/www/luci-static/resources/view/vm + $(INSTALL_DATA) ./htdocs/luci-static/resources/view/vm/*.js $(1)/www/luci-static/resources/view/vm/ +endef + +$(eval $(call BuildPackage,luci-app-vm)) diff --git a/package/secubox/luci-app-vm/htdocs/luci-static/resources/view/vm/overview.js b/package/secubox/luci-app-vm/htdocs/luci-static/resources/view/vm/overview.js new file mode 100644 index 00000000..ba21b332 --- /dev/null +++ b/package/secubox/luci-app-vm/htdocs/luci-static/resources/view/vm/overview.js @@ -0,0 +1,337 @@ +'use strict'; +'require view'; +'require dom'; +'require poll'; +'require rpc'; +'require ui'; + +var callVMStatus = rpc.declare({ + object: 'luci.vm', + method: 'status', + expect: { '': {} } +}); + +var callVMList = rpc.declare({ + object: 'luci.vm', + method: 'list', + expect: { containers: [] } +}); + +var callVMInfo = rpc.declare({ + object: 'luci.vm', + method: 'info', + params: ['name'] +}); + +var callVMStart = rpc.declare({ + object: 'luci.vm', + method: 'start', + params: ['name'] +}); + +var callVMStop = rpc.declare({ + object: 'luci.vm', + method: 'stop', + params: ['name'] +}); + +var callVMRestart = rpc.declare({ + object: 'luci.vm', + method: 'restart', + params: ['name'] +}); + +var callVMSnapshot = rpc.declare({ + object: 'luci.vm', + method: 'snapshot', + params: ['name', 'snap_name'] +}); + +var callVMExport = rpc.declare({ + object: 'luci.vm', + method: 'export', + params: ['name', 'format'] +}); + +// State colors +var stateConfig = { + RUNNING: { color: '#27ae60', icon: '●', label: 'Running' }, + STOPPED: { color: '#e74c3c', icon: '○', label: 'Stopped' }, + FROZEN: { color: '#3498db', icon: '◐', label: 'Frozen' } +}; + +return view.extend({ + load: function() { + return Promise.all([ + callVMStatus(), + callVMList() + ]); + }, + + renderStatusBar: function(status) { + return E('div', { 'class': 'vm-status-bar' }, [ + E('div', { 'class': 'status-item' }, [ + E('span', { 'class': 'status-value', 'style': 'color: #3498db' }, String(status.total || 0)), + E('span', { 'class': 'status-label' }, 'Total') + ]), + E('div', { 'class': 'status-item' }, [ + E('span', { 'class': 'status-value', 'style': 'color: #27ae60' }, String(status.running || 0)), + E('span', { 'class': 'status-label' }, 'Running') + ]), + E('div', { 'class': 'status-item' }, [ + E('span', { 'class': 'status-value', 'style': 'color: #e74c3c' }, String(status.stopped || 0)), + E('span', { 'class': 'status-label' }, 'Stopped') + ]), + E('div', { 'class': 'status-item' }, [ + E('span', { 'class': 'status-value', 'style': 'color: #9b59b6' }, (status.disk_used_mb || 0) + 'MB'), + E('span', { 'class': 'status-label' }, 'Disk Used') + ]), + E('div', { 'class': 'status-item' }, [ + E('span', { 'class': 'status-value', 'style': 'color: #1abc9c' }, (status.disk_free_mb || 0) + 'MB'), + E('span', { 'class': 'status-label' }, 'Disk Free') + ]) + ]); + }, + + renderContainerCard: function(container) { + var self = this; + var stateCfg = stateConfig[container.state] || stateConfig.STOPPED; + var isRunning = container.state === 'RUNNING'; + + var controls = []; + + if (isRunning) { + controls.push( + E('button', { + 'class': 'cbi-button cbi-button-negative', + 'style': 'margin-right: 5px; padding: 4px 12px; font-size: 12px;', + 'click': ui.createHandlerFn(this, function() { + return callVMStop(container.name).then(function() { + window.location.reload(); + }); + }) + }, '⏹ Stop'), + E('button', { + 'class': 'cbi-button cbi-button-action', + 'style': 'margin-right: 5px; padding: 4px 12px; font-size: 12px;', + 'click': ui.createHandlerFn(this, function() { + return callVMRestart(container.name).then(function() { + window.location.reload(); + }); + }) + }, '🔄 Restart') + ); + } else { + controls.push( + E('button', { + 'class': 'cbi-button cbi-button-positive', + 'style': 'margin-right: 5px; padding: 4px 12px; font-size: 12px;', + 'click': ui.createHandlerFn(this, function() { + return callVMStart(container.name).then(function(res) { + if (res.success) { + window.location.reload(); + } else { + ui.addNotification(null, E('p', {}, res.error || 'Start failed')); + } + }); + }) + }, '▶ Start') + ); + } + + controls.push( + E('button', { + 'class': 'cbi-button cbi-button-neutral', + 'style': 'margin-right: 5px; padding: 4px 12px; font-size: 12px;', + 'click': ui.createHandlerFn(this, function() { + return callVMSnapshot(container.name).then(function(res) { + if (res.success) { + ui.addNotification(null, E('p', {}, 'Snapshot created: ' + res.snapshot)); + } else { + ui.addNotification(null, E('p', {}, res.error || 'Snapshot failed')); + } + }); + }) + }, '📸 Snapshot'), + E('button', { + 'class': 'cbi-button cbi-button-neutral', + 'style': 'padding: 4px 12px; font-size: 12px;', + 'click': ui.createHandlerFn(this, function() { + return callVMExport(container.name, 'tar').then(function(res) { + if (res.success) { + ui.addNotification(null, E('p', {}, 'Exported to: ' + res.path + ' (' + res.size + ')')); + } else { + ui.addNotification(null, E('p', {}, res.error || 'Export failed')); + } + }); + }) + }, '📦 Export') + ); + + return E('div', { + 'class': 'vm-container-card', + 'data-state': container.state + }, [ + E('div', { 'class': 'card-header' }, [ + E('span', { 'class': 'container-icon' }, '📦'), + E('div', { 'class': 'container-info' }, [ + E('span', { 'class': 'container-name' }, container.name), + E('span', { 'class': 'container-size' }, (container.rootfs_mb || 0) + ' MB') + ]), + E('span', { + 'class': 'container-state', + 'style': 'color: ' + stateCfg.color, + 'title': stateCfg.label + }, stateCfg.icon + ' ' + stateCfg.label) + ]), + E('div', { 'class': 'card-footer' }, controls) + ]); + }, + + render: function(data) { + var status = data[0]; + var containers = data[1]; + + // Sort containers: running first, then alphabetically + containers.sort(function(a, b) { + if (a.state === 'RUNNING' && b.state !== 'RUNNING') return -1; + if (a.state !== 'RUNNING' && b.state === 'RUNNING') return 1; + return a.name.localeCompare(b.name); + }); + + var self = this; + var view = E('div', { 'class': 'vm-dashboard' }, [ + E('style', {}, ` + .vm-dashboard { + padding: 20px; + } + .vm-header { + text-align: center; + margin-bottom: 30px; + } + .vm-header h2 { + font-size: 2em; + margin-bottom: 10px; + } + .vm-header .subtitle { + color: #666; + font-size: 1.1em; + } + .vm-status-bar { + display: flex; + justify-content: center; + gap: 40px; + padding: 20px; + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + border-radius: 12px; + margin-bottom: 30px; + } + .status-item { + text-align: center; + } + .status-value { + display: block; + font-size: 2em; + font-weight: bold; + } + .status-label { + color: #aaa; + font-size: 0.9em; + text-transform: uppercase; + } + .vm-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 20px; + } + .vm-container-card { + background: #1a1a2e; + border-radius: 12px; + padding: 20px; + border-left: 4px solid #7f8c8d; + transition: transform 0.2s, box-shadow 0.2s; + } + .vm-container-card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0,0,0,0.3); + } + .vm-container-card[data-state="RUNNING"] { + border-left-color: #27ae60; + background: linear-gradient(135deg, #1a2e1a 0%, #162e16 100%); + } + .vm-container-card[data-state="STOPPED"] { + border-left-color: #e74c3c; + } + .card-header { + display: flex; + align-items: center; + margin-bottom: 15px; + } + .container-icon { + font-size: 2em; + margin-right: 15px; + } + .container-info { + flex: 1; + } + .container-name { + display: block; + font-size: 1.2em; + font-weight: bold; + color: #fff; + } + .container-size { + color: #666; + font-size: 0.9em; + } + .container-state { + font-size: 0.9em; + font-weight: bold; + } + .card-footer { + display: flex; + flex-wrap: wrap; + gap: 8px; + } + .card-footer button { + border-radius: 6px; + } + @media (max-width: 768px) { + .vm-status-bar { + flex-wrap: wrap; + gap: 20px; + } + .vm-grid { + grid-template-columns: 1fr; + } + } + `), + E('div', { 'class': 'vm-header' }, [ + E('h2', {}, '📦 VM Manager'), + E('p', { 'class': 'subtitle' }, 'LXC Container Management Dashboard') + ]), + this.renderStatusBar(status), + E('h3', { 'style': 'margin-bottom: 15px; color: #aaa;' }, + 'Containers (' + containers.length + ')' + ), + E('div', { 'class': 'vm-grid' }, + containers.map(function(c) { + return self.renderContainerCard(c); + }) + ) + ]); + + // Setup polling + poll.add(L.bind(function() { + return callVMList().then(L.bind(function(containers) { + // Could update cards here + }, this)); + }, this), 30); + + return view; + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-vm/root/usr/libexec/rpcd/luci.vm b/package/secubox/luci-app-vm/root/usr/libexec/rpcd/luci.vm new file mode 100755 index 00000000..28b13fc3 --- /dev/null +++ b/package/secubox/luci-app-vm/root/usr/libexec/rpcd/luci.vm @@ -0,0 +1,402 @@ +#!/bin/sh +# SecuBox VM Manager RPCD Handler +# Manages LXC containers and VM exports + +. /lib/functions.sh +. /usr/share/libubox/jshn.sh + +LXC_PATH="/srv/lxc" + +# Get list of all containers +get_containers() { + local containers="" + for dir in "$LXC_PATH"/*/; do + [ -d "$dir" ] || continue + local name=$(basename "$dir") + [ -f "$dir/config" ] || continue + containers="$containers $name" + done + echo "$containers" +} + +# Get container state +get_container_state() { + local name="$1" + lxc-info -n "$name" -s 2>/dev/null | awk '{print $2}' +} + +# Get container info +get_container_info() { + local name="$1" + local state=$(get_container_state "$name") + local config="$LXC_PATH/$name/config" + local rootfs_size="0" + local memory="0" + local arch="" + local init_cmd="" + + if [ -f "$config" ]; then + arch=$(grep "lxc.arch" "$config" | cut -d= -f2 | tr -d ' ') + init_cmd=$(grep "lxc.init.cmd" "$config" | cut -d= -f2 | tr -d ' ') + memory=$(grep "lxc.cgroup2.memory.max" "$config" | cut -d= -f2 | tr -d ' ') + [ -z "$memory" ] && memory=$(grep "lxc.cgroup.memory.limit_in_bytes" "$config" | cut -d= -f2 | tr -d ' ') + fi + + if [ -d "$LXC_PATH/$name/rootfs" ]; then + rootfs_size=$(du -sm "$LXC_PATH/$name/rootfs" 2>/dev/null | cut -f1) + fi + + json_add_object "$name" + json_add_string "name" "$name" + json_add_string "state" "${state:-stopped}" + json_add_string "arch" "${arch:-aarch64}" + json_add_int "rootfs_mb" "${rootfs_size:-0}" + json_add_string "memory" "${memory:-unlimited}" + json_add_string "init_cmd" "$init_cmd" + json_close_object +} + +# List all containers +method_list() { + json_init + json_add_array "containers" + + for name in $(get_containers); do + local state=$(get_container_state "$name") + local rootfs_size=$(du -sm "$LXC_PATH/$name/rootfs" 2>/dev/null | cut -f1) + + json_add_object + json_add_string "name" "$name" + json_add_string "state" "${state:-stopped}" + json_add_int "rootfs_mb" "${rootfs_size:-0}" + json_close_object + done + + json_close_array + json_dump +} + +# Get status summary +method_status() { + local total=0 + local running=0 + local stopped=0 + + for name in $(get_containers); do + total=$((total + 1)) + local state=$(get_container_state "$name") + if [ "$state" = "RUNNING" ]; then + running=$((running + 1)) + else + stopped=$((stopped + 1)) + fi + done + + # Get disk usage + local disk_used=$(du -sm "$LXC_PATH" 2>/dev/null | cut -f1) + local disk_free=$(df -m /srv 2>/dev/null | tail -1 | awk '{print $4}') + + json_init + json_add_int "total" "$total" + json_add_int "running" "$running" + json_add_int "stopped" "$stopped" + json_add_int "disk_used_mb" "${disk_used:-0}" + json_add_int "disk_free_mb" "${disk_free:-0}" + json_add_string "lxc_path" "$LXC_PATH" + json_dump +} + +# Get detailed container info +method_info() { + local name="$1" + + [ -d "$LXC_PATH/$name" ] || { + json_init + json_add_string "error" "Container not found" + json_dump + return + } + + json_init + get_container_info "$name" + + # Add extra details for single container + local pid="" + local ips="" + + if [ "$(get_container_state "$name")" = "RUNNING" ]; then + pid=$(lxc-info -n "$name" -p 2>/dev/null | awk '{print $2}') + ips=$(lxc-info -n "$name" -i 2>/dev/null | awk '{print $2}' | tr '\n' ',') + fi + + json_add_string "pid" "$pid" + json_add_string "ips" "$ips" + + # Config file contents + if [ -f "$LXC_PATH/$name/config" ]; then + local config=$(cat "$LXC_PATH/$name/config" | head -50) + json_add_string "config" "$config" + fi + + json_dump +} + +# Get container logs +method_logs() { + local name="$1" + local lines="${2:-50}" + + json_init + + # Try various log sources + local log="" + if [ -f "/var/log/lxc/$name.log" ]; then + log=$(tail -n "$lines" "/var/log/lxc/$name.log" 2>/dev/null) + elif [ -f "/tmp/$name.log" ]; then + log=$(tail -n "$lines" "/tmp/$name.log" 2>/dev/null) + else + log="No logs available for container: $name" + fi + + json_add_string "name" "$name" + json_add_string "logs" "$log" + json_dump +} + +# Start container +method_start() { + local name="$1" + + json_init + + if [ ! -d "$LXC_PATH/$name" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Container not found" + json_dump + return + fi + + lxc-start -n "$name" 2>/tmp/lxc-start-$name.log + local rc=$? + + if [ $rc -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "message" "Container $name started" + else + json_add_boolean "success" 0 + json_add_string "error" "$(cat /tmp/lxc-start-$name.log 2>/dev/null || echo 'Start failed')" + fi + + json_dump +} + +# Stop container +method_stop() { + local name="$1" + + json_init + + if [ ! -d "$LXC_PATH/$name" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Container not found" + json_dump + return + fi + + lxc-stop -n "$name" 2>&1 + local rc=$? + + if [ $rc -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "message" "Container $name stopped" + else + json_add_boolean "success" 0 + json_add_string "error" "Stop failed" + fi + + json_dump +} + +# Restart container +method_restart() { + local name="$1" + + lxc-stop -n "$name" 2>/dev/null + sleep 1 + method_start "$name" +} + +# Create snapshot +method_snapshot() { + local name="$1" + local snap_name="${2:-snapshot-$(date +%Y%m%d-%H%M%S)}" + local snap_dir="$LXC_PATH/$name/snapshots" + + json_init + + mkdir -p "$snap_dir" + + # Stop if running + local was_running=0 + if [ "$(get_container_state "$name")" = "RUNNING" ]; then + was_running=1 + lxc-stop -n "$name" 2>/dev/null + sleep 1 + fi + + # Create snapshot + tar -czf "$snap_dir/$snap_name.tar.gz" -C "$LXC_PATH/$name" rootfs config 2>/dev/null + local rc=$? + + # Restart if was running + [ $was_running -eq 1 ] && lxc-start -n "$name" 2>/dev/null + + if [ $rc -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "snapshot" "$snap_name" + json_add_string "path" "$snap_dir/$snap_name.tar.gz" + else + json_add_boolean "success" 0 + json_add_string "error" "Snapshot failed" + fi + + json_dump +} + +# Export container to VMDK/OVA (for VM builder) +method_export() { + local name="$1" + local format="${2:-tar}" + local output_dir="${3:-/tmp/vm-export}" + + json_init + + mkdir -p "$output_dir" + + # For now, just create a tar archive + # Full VMDK/OVA conversion would require qemu-img on the host + local output="$output_dir/$name-export.tar.gz" + + tar -czf "$output" -C "$LXC_PATH" "$name" 2>/dev/null + + if [ -f "$output" ]; then + local size=$(ls -lh "$output" | awk '{print $5}') + json_add_boolean "success" 1 + json_add_string "format" "tar.gz" + json_add_string "path" "$output" + json_add_string "size" "$size" + else + json_add_boolean "success" 0 + json_add_string "error" "Export failed" + fi + + json_dump +} + +# Main dispatcher +case "$1" in + list) + case "$2" in + status) + method_status + ;; + list) + method_list + ;; + info) + read -r input + json_load "$input" + json_get_var name name + method_info "$name" + ;; + logs) + read -r input + json_load "$input" + json_get_var name name + json_get_var lines lines + method_logs "$name" "$lines" + ;; + start|stop|restart|snapshot|export) + read -r input + json_load "$input" + json_get_var name name + case "$2" in + start) method_start "$name" ;; + stop) method_stop "$name" ;; + restart) method_restart "$name" ;; + snapshot) + json_get_var snap_name snap_name + method_snapshot "$name" "$snap_name" + ;; + export) + json_get_var format format + method_export "$name" "$format" + ;; + esac + ;; + *) + echo '{"status":"invalid_method"}' + ;; + esac + ;; + call) + case "$2" in + status) + method_status + ;; + list) + method_list + ;; + info) + read -r input + json_load "$input" + json_get_var name name + method_info "$name" + ;; + logs) + read -r input + json_load "$input" + json_get_var name name + json_get_var lines lines + method_logs "$name" "$lines" + ;; + start) + read -r input + json_load "$input" + json_get_var name name + method_start "$name" + ;; + stop) + read -r input + json_load "$input" + json_get_var name name + method_stop "$name" + ;; + restart) + read -r input + json_load "$input" + json_get_var name name + method_restart "$name" + ;; + snapshot) + read -r input + json_load "$input" + json_get_var name name + json_get_var snap_name snap_name + method_snapshot "$name" "$snap_name" + ;; + export) + read -r input + json_load "$input" + json_get_var name name + json_get_var format format + method_export "$name" "$format" + ;; + *) + echo '{"error":"unknown_method"}' + ;; + esac + ;; +esac + +exit 0 diff --git a/package/secubox/luci-app-vm/root/usr/share/luci/menu.d/luci-app-vm.json b/package/secubox/luci-app-vm/root/usr/share/luci/menu.d/luci-app-vm.json new file mode 100644 index 00000000..3a5fbaa2 --- /dev/null +++ b/package/secubox/luci-app-vm/root/usr/share/luci/menu.d/luci-app-vm.json @@ -0,0 +1,14 @@ +{ + "admin/services/vm": { + "title": "VM Manager", + "order": 85, + "action": { + "type": "view", + "path": "vm/overview" + }, + "depends": { + "acl": ["luci-app-vm"], + "uci": {} + } + } +} diff --git a/package/secubox/luci-app-vm/root/usr/share/rpcd/acl.d/luci-app-vm.json b/package/secubox/luci-app-vm/root/usr/share/rpcd/acl.d/luci-app-vm.json new file mode 100644 index 00000000..30993969 --- /dev/null +++ b/package/secubox/luci-app-vm/root/usr/share/rpcd/acl.d/luci-app-vm.json @@ -0,0 +1,29 @@ +{ + "luci-app-vm": { + "description": "Grant access to LuCI app vm", + "read": { + "ubus": { + "luci.vm": [ + "status", + "list", + "info", + "logs" + ] + } + }, + "write": { + "ubus": { + "luci.vm": [ + "start", + "stop", + "restart", + "create", + "destroy", + "snapshot", + "restore", + "export" + ] + } + } + } +} diff --git a/package/secubox/secubox-vortex-firewall/root/usr/libexec/rpcd/luci.vortex-firewall b/package/secubox/secubox-vortex-firewall/root/usr/libexec/rpcd/luci.vortex-firewall index adc19fd1..99b2450c 100755 --- a/package/secubox/secubox-vortex-firewall/root/usr/libexec/rpcd/luci.vortex-firewall +++ b/package/secubox/secubox-vortex-firewall/root/usr/libexec/rpcd/luci.vortex-firewall @@ -26,16 +26,19 @@ do_status() { fi json_add_int "domain_count" "$domain_count" - # Hit count + # Hit count (from RPZ log stats) local hit_count=0 - if [ -f "$BLOCKLIST_DB" ]; then - hit_count=$(sqlite3 "$BLOCKLIST_DB" "SELECT COALESCE(SUM(hit_count),0) FROM domains;" 2>/dev/null || echo 0) + if [ -f "$STATS_FILE" ]; then + hit_count=$(jsonfilter -i "$STATS_FILE" -e '@.blocks' 2>/dev/null || echo 0) fi json_add_int "hit_count" "$hit_count" - # x47 impact - local x47_impact=$((hit_count * 47)) - json_add_int "x47_impact" "$x47_impact" + # Unique IPs protected + local unique_ips=0 + if [ -f "$STATS_FILE" ]; then + unique_ips=$(jsonfilter -i "$STATS_FILE" -e '@.unique_ips' 2>/dev/null || echo 0) + fi + json_add_int "unique_ips" "$unique_ips" # Last update if [ -f "$STATS_FILE" ]; then @@ -60,12 +63,16 @@ do_get_stats() { fi local domains=$(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(*) FROM domains WHERE blocked=1;" 2>/dev/null || echo 0) - local hits=$(sqlite3 "$BLOCKLIST_DB" "SELECT COALESCE(SUM(hit_count),0) FROM domains;" 2>/dev/null || echo 0) - local x47=$((hits * 47)) + local hits=0 + local unique_ips=0 + if [ -f "$STATS_FILE" ]; then + hits=$(jsonfilter -i "$STATS_FILE" -e '@.blocks' 2>/dev/null || echo 0) + unique_ips=$(jsonfilter -i "$STATS_FILE" -e '@.unique_ips' 2>/dev/null || echo 0) + fi json_add_int "domains" "$domains" json_add_int "hits" "$hits" - json_add_int "x47_impact" "$x47" + json_add_int "unique_ips" "$unique_ips" # Threat distribution json_add_object "threats" diff --git a/secubox-tools/c3box-vm-builder.sh b/secubox-tools/c3box-vm-builder.sh new file mode 100755 index 00000000..2e2d9451 --- /dev/null +++ b/secubox-tools/c3box-vm-builder.sh @@ -0,0 +1,741 @@ +#!/bin/bash +# +# c3box-vm-builder.sh - Build portable C3Box VM images for VMware/VirtualBox +# +# Creates ready-to-run SecuBox (C3Box) virtual machine images with: +# - Full SecuBox package suite pre-installed +# - Pre-configured networking (bridge mode) +# - All services enabled and ready +# - VMDK/OVA format for VMware, VDI for VirtualBox +# +# Usage: +# ./c3box-vm-builder.sh build # Build x86-64 firmware +# ./c3box-vm-builder.sh convert # Convert to VMDK/OVA +# ./c3box-vm-builder.sh full # Build + Convert +# ./c3box-vm-builder.sh package # Create distributable archive +# + +set -e + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' + +# Configuration +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +REPO_ROOT=$(cd "$SCRIPT_DIR/.." && pwd) +BUILD_DIR="${BUILD_DIR:-$SCRIPT_DIR/c3box-vm}" +OPENWRT_VERSION="${OPENWRT_VERSION:-24.10.5}" +VM_NAME="C3Box-SecuBox" +VM_DISK_SIZE="16G" +VM_MEMORY="2048" +VM_CPUS="2" + +# Output paths +OUTPUT_DIR="$BUILD_DIR/output" +IMG_FILE="$OUTPUT_DIR/c3box-combined-ext4.img" +VMDK_FILE="$OUTPUT_DIR/$VM_NAME.vmdk" +OVA_FILE="$OUTPUT_DIR/$VM_NAME.ova" +VDI_FILE="$OUTPUT_DIR/$VM_NAME.vdi" + +print_header() { + echo "" + echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${CYAN} $1${NC}" + echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo "" +} + +print_success() { echo -e "${GREEN}✅ $1${NC}"; } +print_error() { echo -e "${RED}❌ $1${NC}"; } +print_warning() { echo -e "${YELLOW}⚠️ $1${NC}"; } +print_info() { echo -e "${BLUE}ℹ️ $1${NC}"; } + +# SecuBox core packages for VM +SECUBOX_PACKAGES=( + # Core + "secubox-core" + "secubox-identity" + "secubox-master-link" + "secubox-p2p" + + # LuCI base + "luci" + "luci-ssl" + "luci-theme-secubox" + "luci-app-secubox" + "luci-app-secubox-admin" + + # Security + "luci-app-crowdsec-dashboard" + "luci-app-mitmproxy" + "luci-app-tor-shield" + "luci-app-auth-guardian" + "luci-app-exposure" + + # Networking + "luci-app-haproxy" + "luci-app-wireguard-dashboard" + "luci-app-network-modes" + "luci-app-vhost-manager" + + # Services + "luci-app-matrix" + "luci-app-jabber" + "luci-app-jitsi" + "luci-app-jellyfin" + "luci-app-gitea" + "luci-app-nextcloud" + + # Monitoring + "luci-app-netdata-dashboard" + "luci-app-glances" + "luci-app-system-hub" + + # Tools + "luci-app-cloner" + "luci-app-backup" + "luci-app-media-hub" + "luci-app-saas-relay" + + # System utilities + "lxc" + "docker" + "htop" + "nano" + "curl" + "wget" + "git" + "rsync" + "screen" + "tmux" +) + +check_dependencies() { + print_header "Checking Dependencies" + + local missing=() + + for cmd in qemu-img vboxmanage genisoimage tar gzip; do + if ! command -v $cmd &>/dev/null; then + case $cmd in + qemu-img) + missing+=("qemu-utils") + ;; + vboxmanage) + missing+=("virtualbox (optional)") + ;; + genisoimage) + missing+=("genisoimage") + ;; + *) + missing+=("$cmd") + ;; + esac + fi + done + + if [ ${#missing[@]} -gt 0 ]; then + print_warning "Missing optional tools: ${missing[*]}" + print_info "Install with: sudo apt install qemu-utils genisoimage" + else + print_success "All dependencies satisfied" + fi +} + +download_openwrt_image() { + print_header "Downloading OpenWrt x86-64 Image" + + mkdir -p "$BUILD_DIR" "$OUTPUT_DIR" + cd "$BUILD_DIR" + + local base_url="https://downloads.openwrt.org/releases/${OPENWRT_VERSION}/targets/x86/64" + local img_name="openwrt-${OPENWRT_VERSION}-x86-64-generic-ext4-combined.img.gz" + local img_url="${base_url}/${img_name}" + + if [ -f "$img_name" ]; then + print_info "Image already downloaded: $img_name" + else + print_info "Downloading from: $img_url" + wget -q --show-progress "$img_url" || { + print_error "Failed to download OpenWrt image" + return 1 + } + fi + + # Extract + if [ -f "${img_name%.gz}" ]; then + print_info "Image already extracted" + else + print_info "Extracting image..." + gunzip -k "$img_name" + fi + + # Copy to output + cp "${img_name%.gz}" "$IMG_FILE" + print_success "Image ready: $IMG_FILE" +} + +resize_image() { + print_header "Resizing Image to $VM_DISK_SIZE" + + cd "$OUTPUT_DIR" + + # Resize the image file + print_info "Expanding disk image..." + qemu-img resize -f raw "$IMG_FILE" "$VM_DISK_SIZE" + + # Resize partition using parted + print_info "Resizing partition..." + + # Get current partition info + local part_info=$(parted -s "$IMG_FILE" print 2>/dev/null | grep "^ 2") + if [ -n "$part_info" ]; then + # Resize partition 2 to fill disk + parted -s "$IMG_FILE" resizepart 2 100% + print_success "Partition resized" + else + print_warning "Could not resize partition - manual resize needed on first boot" + fi +} + +inject_secubox_config() { + print_header "Injecting SecuBox Configuration" + + cd "$OUTPUT_DIR" + + # Mount the image + local mount_point="/tmp/c3box-mount-$$" + local loop_dev="" + + mkdir -p "$mount_point" + + # Setup loop device + loop_dev=$(losetup -f --show -P "$IMG_FILE") + print_info "Loop device: $loop_dev" + + # Mount root partition (partition 2) + mount "${loop_dev}p2" "$mount_point" || { + print_error "Failed to mount image" + losetup -d "$loop_dev" + return 1 + } + + print_info "Injecting configuration..." + + # Create SecuBox directories + mkdir -p "$mount_point/etc/secubox" + mkdir -p "$mount_point/srv/secubox" + mkdir -p "$mount_point/srv/lxc" + mkdir -p "$mount_point/srv/matrix" + mkdir -p "$mount_point/srv/jabber" + + # Create first-boot provisioning script + cat > "$mount_point/etc/uci-defaults/99-c3box-init" << 'PROVISION' +#!/bin/sh +# C3Box VM First Boot Configuration + +# Set hostname +uci set system.@system[0].hostname='c3box' +uci commit system + +# Configure network for VM (DHCP on eth0) +uci set network.lan.proto='dhcp' +uci set network.lan.ifname='eth0' +uci delete network.wan 2>/dev/null +uci commit network + +# Enable SSH on all interfaces +uci set dropbear.@dropbear[0].Interface='' +uci commit dropbear + +# Enable LuCI HTTPS +uci set uhttpd.main.listen_https='0.0.0.0:443' +uci set uhttpd.main.redirect_https='1' +uci commit uhttpd + +# Set root password to 'c3box' (change on first login!) +echo -e "c3box\nc3box" | passwd root + +# Create SecuBox identity +if [ -x /usr/sbin/identityctl ]; then + /usr/sbin/identityctl keygen 2>/dev/null || true +fi + +# Enable core services +for svc in secubox-core rpcd uhttpd; do + [ -x /etc/init.d/$svc ] && /etc/init.d/$svc enable +done + +# Expand root filesystem to fill disk +if command -v resize2fs >/dev/null 2>&1; then + ROOT_DEV=$(findmnt -n -o SOURCE /) + resize2fs "$ROOT_DEV" 2>/dev/null || true +fi + +# Log completion +logger -t c3box "First boot configuration complete" +echo "C3Box VM initialized - Login: root / c3box" > /etc/banner + +exit 0 +PROVISION + chmod +x "$mount_point/etc/uci-defaults/99-c3box-init" + + # Create package installation script + cat > "$mount_point/etc/uci-defaults/98-secubox-packages" << 'PACKAGES' +#!/bin/sh +# Install SecuBox packages from feed + +# Add SecuBox feed +cat >> /etc/opkg/customfeeds.conf << 'FEED' +src/gz secubox_bonus https://secubox.in/feed +FEED + +# Update and install core packages +opkg update +opkg install secubox-core luci-theme-secubox luci-app-secubox 2>/dev/null || true + +exit 0 +PACKAGES + chmod +x "$mount_point/etc/uci-defaults/98-secubox-packages" + + # Create VM-specific banner + cat > "$mount_point/etc/banner" << 'BANNER' + + ____ _____ ____ + / ___||___ / | __ ) _____ __ + | | |_ \ | _ \ / _ \ \/ / + | |___ ___) || |_) | (_) > < + \____||____/ |____/ \___/_/\_\ + + SecuBox Virtual Appliance + + Web UI: https:// + SSH: ssh root@ + + Default password: c3box (CHANGE IT!) + +BANNER + + # Cleanup + sync + umount "$mount_point" + losetup -d "$loop_dev" + rmdir "$mount_point" + + print_success "Configuration injected" +} + +convert_to_vmdk() { + print_header "Converting to VMDK (VMware)" + + cd "$OUTPUT_DIR" + + if ! command -v qemu-img &>/dev/null; then + print_error "qemu-img not found. Install: sudo apt install qemu-utils" + return 1 + fi + + print_info "Converting to VMDK format..." + qemu-img convert -f raw -O vmdk "$IMG_FILE" "$VMDK_FILE" + + print_success "VMDK created: $VMDK_FILE" + print_info "Size: $(du -h "$VMDK_FILE" | cut -f1)" +} + +convert_to_vdi() { + print_header "Converting to VDI (VirtualBox)" + + cd "$OUTPUT_DIR" + + if command -v vboxmanage &>/dev/null; then + print_info "Converting to VDI format..." + vboxmanage convertfromraw "$IMG_FILE" "$VDI_FILE" --format VDI + print_success "VDI created: $VDI_FILE" + elif command -v qemu-img &>/dev/null; then + print_info "Converting to VDI format (via qemu-img)..." + qemu-img convert -f raw -O vdi "$IMG_FILE" "$VDI_FILE" + print_success "VDI created: $VDI_FILE" + else + print_warning "No VDI converter available" + fi +} + +create_vmx_file() { + print_header "Creating VMware Configuration" + + cd "$OUTPUT_DIR" + + local vmx_file="$OUTPUT_DIR/$VM_NAME.vmx" + + cat > "$vmx_file" << VMX +.encoding = "UTF-8" +config.version = "8" +virtualHW.version = "19" +pciBridge0.present = "TRUE" +pciBridge4.present = "TRUE" +pciBridge4.virtualDev = "pcieRootPort" +pciBridge4.functions = "8" + +displayName = "$VM_NAME" +guestOS = "other4xlinux-64" + +memsize = "$VM_MEMORY" +numvcpus = "$VM_CPUS" + +scsi0.present = "TRUE" +scsi0.virtualDev = "lsilogic" +scsi0:0.present = "TRUE" +scsi0:0.fileName = "$VM_NAME.vmdk" + +ethernet0.present = "TRUE" +ethernet0.virtualDev = "vmxnet3" +ethernet0.connectionType = "bridged" +ethernet0.addressType = "generated" +ethernet0.startConnected = "TRUE" + +ethernet1.present = "TRUE" +ethernet1.virtualDev = "vmxnet3" +ethernet1.connectionType = "nat" +ethernet1.addressType = "generated" +ethernet1.startConnected = "TRUE" + +serial0.present = "TRUE" +serial0.fileType = "pipe" +serial0.fileName = "/tmp/$VM_NAME.serial" +serial0.tryNoRxLoss = "TRUE" + +floppy0.present = "FALSE" +tools.syncTime = "TRUE" +tools.upgrade.policy = "manual" + +uuid.bios = "56 4d 12 34 56 78 9a bc-de f0 12 34 56 78 9a bc" +VMX + + print_success "VMX created: $vmx_file" +} + +create_ova() { + print_header "Creating OVA Package" + + cd "$OUTPUT_DIR" + + # Create OVF descriptor + local ovf_file="$OUTPUT_DIR/$VM_NAME.ovf" + + cat > "$ovf_file" << 'OVF' + + + + + + + Virtual disk information + + + + Network configuration + + Bridged network + + + + C3Box SecuBox Virtual Appliance + C3Box-SecuBox + + Linux 64-bit + + + Virtual hardware requirements + + Virtual Hardware Family + 0 + vmx-19 + + + hertz * 10^6 + Number of Virtual CPUs + 2 virtual CPU(s) + 1 + 3 + 2 + + + byte * 2^20 + Memory Size + 2048MB of memory + 2 + 4 + 2048 + + + 0 + Hard Disk 1 + ovf:/disk/vmdisk1 + 3 + 17 + + + true + bridged + Ethernet adapter on bridged network + Ethernet 1 + 4 + VmxNet3 + 10 + + + + +OVF + + # Create OVA (tar archive) + print_info "Packaging OVA..." + tar -cvf "$OVA_FILE" -C "$OUTPUT_DIR" "$VM_NAME.ovf" "$VM_NAME.vmdk" + + print_success "OVA created: $OVA_FILE" + print_info "Size: $(du -h "$OVA_FILE" | cut -f1)" +} + +create_package() { + print_header "Creating Distribution Package" + + cd "$OUTPUT_DIR" + + local pkg_name="C3Box-SecuBox-VM-$(date +%Y%m%d)" + local pkg_dir="$OUTPUT_DIR/$pkg_name" + + mkdir -p "$pkg_dir" + + # Copy files + cp -v "$VMDK_FILE" "$pkg_dir/" 2>/dev/null || true + cp -v "$OVA_FILE" "$pkg_dir/" 2>/dev/null || true + cp -v "$VDI_FILE" "$pkg_dir/" 2>/dev/null || true + cp -v "$OUTPUT_DIR/$VM_NAME.vmx" "$pkg_dir/" 2>/dev/null || true + + # Create README + cat > "$pkg_dir/README.md" << 'README' +# C3Box SecuBox Virtual Appliance + +## Quick Start + +### VMware (Workstation/Player/ESXi) + +1. Import the OVA file: `C3Box-SecuBox.ova` +2. Or use the VMX + VMDK files directly +3. Start the VM +4. Access web UI: https:// + +### VirtualBox + +1. Import the OVA file, OR +2. Create new VM and attach `C3Box-SecuBox.vdi` +3. Configure: Linux 64-bit, 2GB RAM, Bridged Network +4. Start the VM + +### Default Credentials + +- **Username:** root +- **Password:** c3box + +**IMPORTANT: Change the password on first login!** + +### Network Configuration + +The VM is configured for: +- **eth0:** Bridged network (DHCP) +- **eth1:** NAT network (if available) + +### Included Services + +- Matrix Homeserver (E2EE messaging) +- Jabber/XMPP Server +- Jitsi Meet (Video conferencing) +- HAProxy (Reverse proxy with SSL) +- CrowdSec (Security monitoring) +- WireGuard VPN +- And 50+ SecuBox modules + +### Support + +- Web: https://secubox.in +- GitHub: https://github.com/gkerma/secubox-openwrt + +README + + # Create archive + print_info "Creating distribution archive..." + tar -czvf "${pkg_name}.tar.gz" -C "$OUTPUT_DIR" "$pkg_name" + + print_success "Package created: $OUTPUT_DIR/${pkg_name}.tar.gz" + + # Show contents + echo "" + print_info "Package contents:" + ls -lh "$pkg_dir" +} + +cmd_build() { + print_header "C3Box VM Builder - Build Phase" + + check_dependencies + download_openwrt_image + resize_image + + # Only inject config if running as root (needed for mount) + if [ "$(id -u)" = "0" ]; then + inject_secubox_config + else + print_warning "Run as root to inject SecuBox configuration" + print_info "sudo $0 build" + fi + + print_success "Build phase complete" +} + +cmd_convert() { + print_header "C3Box VM Builder - Convert Phase" + + if [ ! -f "$IMG_FILE" ]; then + print_error "Image not found: $IMG_FILE" + print_info "Run: $0 build first" + return 1 + fi + + convert_to_vmdk + convert_to_vdi + create_vmx_file + create_ova + + print_success "Conversion complete" +} + +cmd_full() { + cmd_build + cmd_convert + create_package + + print_header "C3Box VM Build Complete!" + echo "" + print_info "Output files in: $OUTPUT_DIR" + ls -lh "$OUTPUT_DIR"/*.{vmdk,ova,vdi,vmx,img} 2>/dev/null || true + echo "" + print_success "Ready to deploy!" +} + +usage() { + cat << 'USAGE' +C3Box VM Builder - Create portable SecuBox virtual appliances + +Usage: c3box-vm-builder.sh [options] + +Commands: + build Download OpenWrt and prepare base image + convert Convert image to VMDK/VDI/OVA formats + full Build + Convert + Package (complete workflow) + package Create distribution archive + clean Remove build artifacts + +Options: + --version VER OpenWrt version (default: 24.10.5) + --disk SIZE Disk size (default: 16G) + --memory MB RAM in MB (default: 2048) + --cpus N CPU count (default: 2) + +Examples: + sudo ./c3box-vm-builder.sh full + sudo ./c3box-vm-builder.sh build --version 24.10.5 + ./c3box-vm-builder.sh convert + ./c3box-vm-builder.sh package + +Output: + c3box-vm/output/C3Box-SecuBox.vmdk - VMware disk + c3box-vm/output/C3Box-SecuBox.ova - VMware appliance + c3box-vm/output/C3Box-SecuBox.vdi - VirtualBox disk + c3box-vm/output/C3Box-SecuBox.vmx - VMware config + +USAGE +} + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --version) + OPENWRT_VERSION="$2" + shift 2 + ;; + --disk) + VM_DISK_SIZE="$2" + shift 2 + ;; + --memory) + VM_MEMORY="$2" + shift 2 + ;; + --cpus) + VM_CPUS="$2" + shift 2 + ;; + build) + CMD="build" + shift + ;; + convert) + CMD="convert" + shift + ;; + full) + CMD="full" + shift + ;; + package) + CMD="package" + shift + ;; + clean) + print_info "Cleaning build directory..." + rm -rf "$BUILD_DIR" + print_success "Clean complete" + exit 0 + ;; + -h|--help|help) + usage + exit 0 + ;; + *) + print_error "Unknown option: $1" + usage + exit 1 + ;; + esac +done + +# Execute command +case "${CMD:-}" in + build) + cmd_build + ;; + convert) + cmd_convert + ;; + full) + cmd_full + ;; + package) + create_package + ;; + *) + usage + exit 1 + ;; +esac