From c6fb79ed3b6480d413d80ef44896d9edf62c73d8 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Thu, 5 Feb 2026 10:40:32 +0100 Subject: [PATCH] feat: Add unified backup manager, custom mail server, DNS subdomain generator New packages: - secubox-app-backup: Unified backup for LXC containers, UCI config, services - luci-app-backup: KISS dashboard with container list and backup history - secubox-app-mailserver: Custom Postfix+Dovecot in LXC with mesh backup Enhanced dnsctl with: - generate: Auto-create subdomain A records - suggest: Name suggestions by category - mail-setup: MX, SPF, DMARC record creation - dkim-add: DKIM TXT record management Co-Authored-By: Claude Opus 4.5 --- .claude/HISTORY.md | 29 ++ .claude/WIP.md | 18 + package/secubox/luci-app-backup/Makefile | 29 ++ package/secubox/luci-app-backup/README.md | 39 ++ .../resources/view/backup/overview.js | 235 ++++++++++ .../root/usr/libexec/rpcd/luci.backup | 240 ++++++++++ .../share/luci/menu.d/luci-app-backup.json | 14 + .../usr/share/rpcd/acl.d/luci-app-backup.json | 17 + package/secubox/secubox-app-backup/Makefile | 41 ++ package/secubox/secubox-app-backup/README.md | 130 ++++++ .../files/etc/config/backup | 25 + .../files/usr/lib/backup/config.sh | 132 ++++++ .../files/usr/lib/backup/containers.sh | 148 ++++++ .../files/usr/lib/backup/remote.sh | 139 ++++++ .../files/usr/sbin/secubox-backup | 437 +++++++++++++++++ .../secubox-app-dns-provider/README.md | 45 +- .../files/usr/sbin/dnsctl | 176 +++++++ .../secubox/secubox-app-mailserver/Makefile | 39 ++ .../secubox/secubox-app-mailserver/README.md | 208 +++++++++ .../files/etc/config/mailserver | 44 ++ .../files/etc/init.d/mailserver | 59 +++ .../files/usr/lib/mailserver/container.sh | 133 ++++++ .../files/usr/lib/mailserver/mesh.sh | 115 +++++ .../files/usr/lib/mailserver/users.sh | 159 +++++++ .../files/usr/sbin/mailctl | 442 ++++++++++++++++++ 25 files changed, 3091 insertions(+), 2 deletions(-) create mode 100644 package/secubox/luci-app-backup/Makefile create mode 100644 package/secubox/luci-app-backup/README.md create mode 100644 package/secubox/luci-app-backup/htdocs/luci-static/resources/view/backup/overview.js create mode 100644 package/secubox/luci-app-backup/root/usr/libexec/rpcd/luci.backup create mode 100644 package/secubox/luci-app-backup/root/usr/share/luci/menu.d/luci-app-backup.json create mode 100644 package/secubox/luci-app-backup/root/usr/share/rpcd/acl.d/luci-app-backup.json create mode 100644 package/secubox/secubox-app-backup/Makefile create mode 100644 package/secubox/secubox-app-backup/README.md create mode 100644 package/secubox/secubox-app-backup/files/etc/config/backup create mode 100644 package/secubox/secubox-app-backup/files/usr/lib/backup/config.sh create mode 100644 package/secubox/secubox-app-backup/files/usr/lib/backup/containers.sh create mode 100644 package/secubox/secubox-app-backup/files/usr/lib/backup/remote.sh create mode 100644 package/secubox/secubox-app-backup/files/usr/sbin/secubox-backup create mode 100644 package/secubox/secubox-app-mailserver/Makefile create mode 100644 package/secubox/secubox-app-mailserver/README.md create mode 100644 package/secubox/secubox-app-mailserver/files/etc/config/mailserver create mode 100644 package/secubox/secubox-app-mailserver/files/etc/init.d/mailserver create mode 100644 package/secubox/secubox-app-mailserver/files/usr/lib/mailserver/container.sh create mode 100644 package/secubox/secubox-app-mailserver/files/usr/lib/mailserver/mesh.sh create mode 100644 package/secubox/secubox-app-mailserver/files/usr/lib/mailserver/users.sh create mode 100644 package/secubox/secubox-app-mailserver/files/usr/sbin/mailctl diff --git a/.claude/HISTORY.md b/.claude/HISTORY.md index 7bb7bc53..49ad1ad7 100644 --- a/.claude/HISTORY.md +++ b/.claude/HISTORY.md @@ -347,3 +347,32 @@ _Last updated: 2026-02-06_ - `.ta-cve-link` for NVD hyperlinks (red badge style) - `.ta-cve-row` for highlighted CVE threat rows - Following LuCI UI Generation Model Template v0.1.0 for future KISS modules. + +33. **Unified Backup Manager & Custom Mail Server (2026-02-05)** + - Created `secubox-app-backup` — unified backup system for LXC containers, UCI config, service data. + - **CLI commands**: create (full/config/containers/services), list, restore, status, cleanup + - **Container ops**: container list/backup/restore/backups + - **Profile ops**: profile list/create/apply/share (delegates to secubox-profile) + - **Remote sync**: sync --push/--pull (Gitea integration) + - **Libraries**: containers.sh, config.sh, remote.sh + - **Storage structure**: /srv/backups/{config,containers,services,profiles} + - Created `luci-app-backup` — LuCI dashboard for backup management. + - **Status panel**: storage path, usage, last backup times + - **Quick actions**: Full/Config/Containers backup buttons + - **Container table**: name, state, size, backup count, backup button + - **Backup history**: file, type, size, date (sorted by timestamp) + - **RPCD methods**: status, list, container_list, create, restore, cleanup, container_backup, container_restore + - Created `secubox-app-mailserver` — custom Postfix + Dovecot mail server in LXC container. + - **mailctl CLI**: install, start/stop/restart, status + - **User management**: user add/del/list/passwd, alias add/list + - **SSL**: ssl-setup (ACME DNS-01), ssl-status + - **DNS integration**: dns-setup (creates MX, SPF, DMARC via dnsctl) + - **Mesh backup**: mesh backup/restore/sync/add-peer/peers/enable/disable + - **Webmail integration**: webmail status/configure (Roundcube container) + - **Libraries**: container.sh, users.sh, mesh.sh + - Enhanced `dnsctl` with subdomain generation and mail DNS: + - `generate [prefix]` — auto-create subdomain A record with public IP + - `suggest [category]` — subdomain name suggestions (web, mail, dev, media, iot, security) + - `mail-setup [host] [priority]` — create MX, SPF, DMARC records + - `dkim-add [selector] ` — add DKIM TXT record + - Renamed `secbx-webmail` Docker container to `secubox-webmail` for consistency. diff --git a/.claude/WIP.md b/.claude/WIP.md index 764933fb..991ecfbc 100644 --- a/.claude/WIP.md +++ b/.claude/WIP.md @@ -47,6 +47,24 @@ _None currently active_ ### Just Completed +- **Unified Backup Manager** — DONE (2026-02-05) + - Created `secubox-app-backup` CLI for LXC containers, UCI config, service data + - Created `luci-app-backup` dashboard with container list, backup history + - Gitea remote sync and mesh backup support + - RPCD handler with 8 methods + +- **Custom Mail Server** — DONE (2026-02-05) + - Created `secubox-app-mailserver` - Postfix + Dovecot in LXC container + - `mailctl` CLI: user management, aliases, SSL, mesh backup + - Webmail (Roundcube) integration + - Mesh P2P mail backup sync + +- **DNS Provider Enhanced** — DONE (2026-02-05) + - Added `dnsctl generate` - auto-generate subdomain A records + - Added `dnsctl suggest` - name suggestions by category + - Added `dnsctl mail-setup` - MX, SPF, DMARC records + - Added `dnsctl dkim-add` - DKIM TXT record + - **Subdomain Generator Tool** — DONE (2026-02-05) - `secubox-subdomain` CLI for generative subdomain management - Automates: DNS A record + HAProxy vhost + UCI registration diff --git a/package/secubox/luci-app-backup/Makefile b/package/secubox/luci-app-backup/Makefile new file mode 100644 index 00000000..5eabc3c8 --- /dev/null +++ b/package/secubox/luci-app-backup/Makefile @@ -0,0 +1,29 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-backup +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=SecuBox Team +PKG_LICENSE:=MIT + +LUCI_TITLE:=LuCI Backup Manager +LUCI_DEPENDS:=+secubox-app-backup +luci-base + +include $(TOPDIR)/feeds/luci/luci.mk + +define Package/$(PKG_NAME)/install + $(INSTALL_DIR) $(1)/usr/share/luci/menu.d + $(INSTALL_DATA) ./root/usr/share/luci/menu.d/*.json $(1)/usr/share/luci/menu.d/ + + $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d + $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/*.json $(1)/usr/share/rpcd/acl.d/ + + $(INSTALL_DIR) $(1)/usr/libexec/rpcd + $(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.backup $(1)/usr/libexec/rpcd/ + + $(INSTALL_DIR) $(1)/www/luci-static/resources/view/backup + $(INSTALL_DATA) ./htdocs/luci-static/resources/view/backup/*.js $(1)/www/luci-static/resources/view/backup/ +endef + +$(eval $(call BuildPackage,$(PKG_NAME))) diff --git a/package/secubox/luci-app-backup/README.md b/package/secubox/luci-app-backup/README.md new file mode 100644 index 00000000..d92a8f3a --- /dev/null +++ b/package/secubox/luci-app-backup/README.md @@ -0,0 +1,39 @@ +# LuCI Backup Manager + +Web dashboard for SecuBox backup management. + +## Features + +- Backup status overview (storage path, usage, last backup times) +- Quick action buttons for full/config/container backups +- LXC container list with state, size, and backup count +- Backup history table with file, type, size, and date +- One-click container backup + +## Installation + +```bash +opkg install luci-app-backup +``` + +## Location + +**System → Backup Manager** + +## RPCD Methods + +| Method | Parameters | Description | +|--------|------------|-------------| +| `status` | - | Get backup status and stats | +| `list` | `type` | List backups (all/config/containers/services) | +| `container_list` | - | List LXC containers with backup info | +| `create` | `type` | Create backup (full/config/containers/services) | +| `restore` | `file`, `dry_run` | Restore from backup file | +| `cleanup` | - | Remove old backups | +| `container_backup` | `name` | Backup specific container | +| `container_restore` | `name`, `file` | Restore specific container | + +## Dependencies + +- `secubox-app-backup` - Backend CLI +- `luci-base` - LuCI framework diff --git a/package/secubox/luci-app-backup/htdocs/luci-static/resources/view/backup/overview.js b/package/secubox/luci-app-backup/htdocs/luci-static/resources/view/backup/overview.js new file mode 100644 index 00000000..8983d13c --- /dev/null +++ b/package/secubox/luci-app-backup/htdocs/luci-static/resources/view/backup/overview.js @@ -0,0 +1,235 @@ +'use strict'; +'require view'; +'require rpc'; +'require ui'; +'require poll'; + +var callStatus = rpc.declare({ + object: 'luci.backup', + method: 'status', + expect: {} +}); + +var callList = rpc.declare({ + object: 'luci.backup', + method: 'list', + params: ['type'], + expect: {} +}); + +var callContainerList = rpc.declare({ + object: 'luci.backup', + method: 'container_list', + expect: {} +}); + +var callCreate = rpc.declare({ + object: 'luci.backup', + method: 'create', + params: ['type'], + expect: {} +}); + +var callContainerBackup = rpc.declare({ + object: 'luci.backup', + method: 'container_backup', + params: ['name'], + expect: {} +}); + +function formatDate(ts) { + if (!ts || ts === 0) return '-'; + var d = new Date(ts * 1000); + return d.toLocaleString(); +} + +return view.extend({ + load: function() { + return Promise.all([ + callStatus(), + callList('all'), + callContainerList() + ]); + }, + + render: function(data) { + var status = data[0] || {}; + var backups = (data[1] || {}).backups || []; + var containers = (data[2] || {}).containers || []; + + var view = E('div', { 'class': 'cbi-map' }, [ + E('h2', {}, 'Backup Manager'), + + // Status Card + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, 'Status'), + E('table', { 'class': 'table' }, [ + E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td', 'style': 'width:200px' }, 'Storage Path'), + E('td', { 'class': 'td' }, status.storage_path || '/srv/backups') + ]), + E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, 'Storage Used'), + E('td', { 'class': 'td' }, status.storage_used || '0') + ]), + E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, 'Containers'), + E('td', { 'class': 'td' }, String(status.container_count || 0)) + ]) + ]) + ]), + + // Quick Actions + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, 'Quick Actions'), + E('div', { 'style': 'display:flex;gap:10px;flex-wrap:wrap' }, [ + E('button', { + 'class': 'btn cbi-button-action', + 'click': ui.createHandlerFn(this, function() { + return this.doBackup('full'); + }) + }, 'Full Backup'), + E('button', { + 'class': 'btn cbi-button-neutral', + 'click': ui.createHandlerFn(this, function() { + return this.doBackup('config'); + }) + }, 'Config Only'), + E('button', { + 'class': 'btn cbi-button-neutral', + 'click': ui.createHandlerFn(this, function() { + return this.doBackup('containers'); + }) + }, 'Containers Only') + ]) + ]), + + // Containers Section + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, 'LXC Containers'), + this.renderContainerTable(containers) + ]), + + // Backup History + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, 'Backup History'), + this.renderBackupTable(backups) + ]) + ]); + + return view; + }, + + renderContainerTable: function(containers) { + if (!containers || containers.length === 0) { + return E('p', { 'class': 'cbi-value-description' }, 'No LXC containers found.'); + } + + var rows = containers.map(L.bind(function(c) { + var stateClass = c.state === 'running' ? 'badge success' : 'badge'; + return E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, c.name), + E('td', { 'class': 'td' }, [ + E('span', { 'style': c.state === 'running' ? 'color:green' : 'color:gray' }, + c.state === 'running' ? '● Running' : '○ Stopped') + ]), + E('td', { 'class': 'td' }, c.size || '-'), + E('td', { 'class': 'td' }, String(c.backups || 0)), + E('td', { 'class': 'td' }, [ + E('button', { + 'class': 'btn cbi-button-action', + 'style': 'padding:2px 8px;font-size:12px', + 'click': ui.createHandlerFn(this, function(name) { + return this.doContainerBackup(name); + }, c.name) + }, 'Backup') + ]) + ]); + }, this)); + + return E('table', { 'class': 'table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, 'Name'), + E('th', { 'class': 'th' }, 'State'), + E('th', { 'class': 'th' }, 'Size'), + E('th', { 'class': 'th' }, 'Backups'), + E('th', { 'class': 'th' }, 'Actions') + ]) + ].concat(rows)); + }, + + renderBackupTable: function(backups) { + if (!backups || backups.length === 0) { + return E('p', { 'class': 'cbi-value-description' }, 'No backups found.'); + } + + // Sort by timestamp descending + backups.sort(function(a, b) { return (b.timestamp || 0) - (a.timestamp || 0); }); + + var rows = backups.slice(0, 20).map(function(b) { + var typeLabel = { + 'config': 'Config', + 'container': 'Container', + 'service': 'Service' + }[b.type] || b.type; + + return E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, b.file), + E('td', { 'class': 'td' }, typeLabel), + E('td', { 'class': 'td' }, b.size || '-'), + E('td', { 'class': 'td' }, formatDate(b.timestamp)) + ]); + }); + + return E('table', { 'class': 'table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, 'File'), + E('th', { 'class': 'th' }, 'Type'), + E('th', { 'class': 'th' }, 'Size'), + E('th', { 'class': 'th' }, 'Date') + ]) + ].concat(rows)); + }, + + doBackup: function(type) { + ui.showModal('Creating Backup', [ + E('p', { 'class': 'spinning' }, 'Creating ' + type + ' backup...') + ]); + + return callCreate(type).then(function(res) { + ui.hideModal(); + if (res.code === 0) { + ui.addNotification(null, E('p', 'Backup created successfully.'), 'success'); + } else { + ui.addNotification(null, E('p', 'Backup failed: ' + (res.output || 'Unknown error')), 'error'); + } + window.location.reload(); + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', 'Error: ' + err.message), 'error'); + }); + }, + + doContainerBackup: function(name) { + ui.showModal('Backing Up Container', [ + E('p', { 'class': 'spinning' }, 'Backing up container: ' + name + '...') + ]); + + return callContainerBackup(name).then(function(res) { + ui.hideModal(); + if (res.code === 0) { + ui.addNotification(null, E('p', 'Container backup created.'), 'success'); + } else { + ui.addNotification(null, E('p', 'Backup failed: ' + (res.output || 'Unknown error')), 'error'); + } + window.location.reload(); + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', 'Error: ' + err.message), 'error'); + }); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-backup/root/usr/libexec/rpcd/luci.backup b/package/secubox/luci-app-backup/root/usr/libexec/rpcd/luci.backup new file mode 100644 index 00000000..b12330ee --- /dev/null +++ b/package/secubox/luci-app-backup/root/usr/libexec/rpcd/luci.backup @@ -0,0 +1,240 @@ +#!/bin/sh +# SecuBox Backup Manager RPCD Handler + +. /usr/share/libubox/jshn.sh + +BACKUP_CMD="/usr/sbin/secubox-backup" + +case "$1" in + list) + cat <<-EOF + { + "status": {}, + "list": { "type": "string" }, + "container_list": {}, + "create": { "type": "string" }, + "restore": { "file": "string", "dry_run": "boolean" }, + "cleanup": {}, + "container_backup": { "name": "string" }, + "container_restore": { "name": "string", "file": "string" } + } + EOF + ;; + + call) + case "$2" in + status) + json_init + + # Get storage info + local storage=$($BACKUP_CMD status 2>/dev/null | grep "Storage Path" | cut -d: -f2 | tr -d ' ') + local used=$($BACKUP_CMD status 2>/dev/null | grep "Storage Used" | cut -d: -f2 | tr -d ' ') + + json_add_string "storage_path" "${storage:-/srv/backups}" + json_add_string "storage_used" "${used:-0}" + + # Last backup times + json_add_object "last_backup" + for type in config containers services; do + local latest=$(ls -t "${storage:-/srv/backups}/$type/"*.tar* 2>/dev/null | head -1) + if [ -n "$latest" ]; then + local date=$(stat -c %Y "$latest" 2>/dev/null) + json_add_int "$type" "${date:-0}" + else + json_add_int "$type" 0 + fi + done + json_close_object + + # Container count + local containers=$(ls -d /srv/lxc/*/ 2>/dev/null | wc -l) + json_add_int "container_count" "$containers" + + json_dump + ;; + + list) + json_load "$3" + json_get_var type type + type="${type:-all}" + + local storage=$(uci -q get backup.main.storage_path) + storage="${storage:-/srv/backups}" + + json_init + json_add_array "backups" + + # List config backups + if [ "$type" = "all" ] || [ "$type" = "config" ]; then + for f in "$storage/config/"*.tar* 2>/dev/null; do + [ -f "$f" ] || continue + json_add_object "" + json_add_string "file" "$(basename "$f")" + json_add_string "type" "config" + json_add_string "size" "$(du -h "$f" 2>/dev/null | cut -f1)" + json_add_int "timestamp" "$(stat -c %Y "$f" 2>/dev/null)" + json_close_object + done + fi + + # List container backups + if [ "$type" = "all" ] || [ "$type" = "containers" ]; then + for f in "$storage/containers/"*.tar* 2>/dev/null; do + [ -f "$f" ] || continue + json_add_object "" + json_add_string "file" "$(basename "$f")" + json_add_string "type" "container" + json_add_string "size" "$(du -h "$f" 2>/dev/null | cut -f1)" + json_add_int "timestamp" "$(stat -c %Y "$f" 2>/dev/null)" + json_close_object + done + fi + + # List service backups + if [ "$type" = "all" ] || [ "$type" = "services" ]; then + for f in "$storage/services/"*.tar* 2>/dev/null; do + [ -f "$f" ] || continue + json_add_object "" + json_add_string "file" "$(basename "$f")" + json_add_string "type" "service" + json_add_string "size" "$(du -h "$f" 2>/dev/null | cut -f1)" + json_add_int "timestamp" "$(stat -c %Y "$f" 2>/dev/null)" + json_close_object + done + fi + + json_close_array + json_dump + ;; + + container_list) + json_init + json_add_array "containers" + + for dir in /srv/lxc/*/; do + [ -d "$dir" ] || continue + local name=$(basename "$dir") + [ -f "$dir/config" ] || continue + + local state="stopped" + lxc-info -n "$name" 2>/dev/null | grep -q "RUNNING" && state="running" + local size=$(du -sh "$dir" 2>/dev/null | awk '{print $1}') + + # Count backups for this container + local storage=$(uci -q get backup.main.storage_path) + storage="${storage:-/srv/backups}" + local backup_count=$(ls -1 "$storage/containers/${name}-"*.tar* 2>/dev/null | wc -l) + + json_add_object "" + json_add_string "name" "$name" + json_add_string "state" "$state" + json_add_string "size" "$size" + json_add_int "backups" "$backup_count" + json_close_object + done + + json_close_array + json_dump + ;; + + create) + json_load "$3" + json_get_var type type + type="${type:-full}" + + local result + result=$($BACKUP_CMD create --$type 2>&1) + local rc=$? + + json_init + json_add_int "code" "$rc" + json_add_string "output" "$result" + json_dump + ;; + + restore) + json_load "$3" + json_get_var file file + json_get_var dry_run dry_run + + [ -z "$file" ] && { + json_init + json_add_int "code" 1 + json_add_string "error" "No file specified" + json_dump + exit 0 + } + + local opts="" + [ "$dry_run" = "1" ] || [ "$dry_run" = "true" ] && opts="--dry-run" + + local result + result=$($BACKUP_CMD restore "$file" $opts 2>&1) + local rc=$? + + json_init + json_add_int "code" "$rc" + json_add_string "output" "$result" + json_dump + ;; + + cleanup) + local result + result=$($BACKUP_CMD cleanup 2>&1) + local rc=$? + + json_init + json_add_int "code" "$rc" + json_add_string "output" "$result" + json_dump + ;; + + container_backup) + json_load "$3" + json_get_var name name + + [ -z "$name" ] && { + json_init + json_add_int "code" 1 + json_add_string "error" "No container name specified" + json_dump + exit 0 + } + + local result + result=$($BACKUP_CMD container backup "$name" 2>&1) + local rc=$? + + json_init + json_add_int "code" "$rc" + json_add_string "output" "$result" + json_dump + ;; + + container_restore) + json_load "$3" + json_get_var name name + json_get_var file file + + [ -z "$name" ] || [ -z "$file" ] && { + json_init + json_add_int "code" 1 + json_add_string "error" "Container name and backup file required" + json_dump + exit 0 + } + + local result + result=$($BACKUP_CMD container restore "$name" "$file" 2>&1) + local rc=$? + + json_init + json_add_int "code" "$rc" + json_add_string "output" "$result" + json_dump + ;; + esac + ;; +esac + +exit 0 diff --git a/package/secubox/luci-app-backup/root/usr/share/luci/menu.d/luci-app-backup.json b/package/secubox/luci-app-backup/root/usr/share/luci/menu.d/luci-app-backup.json new file mode 100644 index 00000000..192961d2 --- /dev/null +++ b/package/secubox/luci-app-backup/root/usr/share/luci/menu.d/luci-app-backup.json @@ -0,0 +1,14 @@ +{ + "admin/system/backup": { + "title": "Backup Manager", + "order": 85, + "action": { + "type": "view", + "path": "backup/overview" + }, + "depends": { + "acl": ["luci-app-backup"], + "uci": { "backup": true } + } + } +} diff --git a/package/secubox/luci-app-backup/root/usr/share/rpcd/acl.d/luci-app-backup.json b/package/secubox/luci-app-backup/root/usr/share/rpcd/acl.d/luci-app-backup.json new file mode 100644 index 00000000..75d9f0e0 --- /dev/null +++ b/package/secubox/luci-app-backup/root/usr/share/rpcd/acl.d/luci-app-backup.json @@ -0,0 +1,17 @@ +{ + "luci-app-backup": { + "description": "Grant access to Backup Manager", + "read": { + "ubus": { + "luci.backup": ["status", "list", "container_list"] + }, + "uci": ["backup"] + }, + "write": { + "ubus": { + "luci.backup": ["create", "restore", "cleanup", "container_backup", "container_restore"] + }, + "uci": ["backup"] + } + } +} diff --git a/package/secubox/secubox-app-backup/Makefile b/package/secubox/secubox-app-backup/Makefile new file mode 100644 index 00000000..8a32e536 --- /dev/null +++ b/package/secubox/secubox-app-backup/Makefile @@ -0,0 +1,41 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=secubox-app-backup +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 +PKG_MAINTAINER:=SecuBox +PKG_LICENSE:=GPL-3.0 + +PKG_FLAGS:=nonshared + +include $(INCLUDE_DIR)/package.mk + +define Package/secubox-app-backup + SECTION:=secubox + CATEGORY:=SecuBox + TITLE:=SecuBox Unified Backup Manager + DEPENDS:=+secubox-core +tar +gzip + PKGARCH:=all +endef + +define Package/secubox-app-backup/description + Unified backup and restore system for SecuBox. + Supports LXC containers, UCI configs, service data, and profiles. +endef + +define Package/secubox-app-backup/conffiles +/etc/config/backup +endef + +define Package/secubox-app-backup/install + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) ./files/etc/config/backup $(1)/etc/config/backup + $(INSTALL_DIR) $(1)/etc/cron.d + $(INSTALL_DATA) ./files/etc/cron.d/secubox-backup $(1)/etc/cron.d/secubox-backup + $(INSTALL_DIR) $(1)/usr/sbin + $(INSTALL_BIN) ./files/usr/sbin/secubox-backup $(1)/usr/sbin/secubox-backup + $(INSTALL_DIR) $(1)/usr/lib/backup + $(INSTALL_DATA) ./files/usr/lib/backup/*.sh $(1)/usr/lib/backup/ +endef + +$(eval $(call BuildPackage,secubox-app-backup)) diff --git a/package/secubox/secubox-app-backup/README.md b/package/secubox/secubox-app-backup/README.md new file mode 100644 index 00000000..65c6601b --- /dev/null +++ b/package/secubox/secubox-app-backup/README.md @@ -0,0 +1,130 @@ +# SecuBox Backup Manager + +Unified backup system for LXC containers, UCI configuration, service data, and profiles with mesh sync support. + +## Installation + +```bash +opkg install secubox-app-backup +``` + +## CLI Usage + +### Create Backups + +```bash +# Full backup (config + containers + services) +secubox-backup create --full + +# Config only (UCI, certificates) +secubox-backup create --config + +# Containers only (LXC) +secubox-backup create --containers + +# Services only (HAProxy, mitmproxy data) +secubox-backup create --services +``` + +### List Backups + +```bash +secubox-backup list # All backups +secubox-backup list --local # Local only +secubox-backup list --remote # Gitea remote only +``` + +### Restore + +```bash +secubox-backup restore config-20260205-120000.tar.gz +secubox-backup restore config-20260205-120000.tar.gz --dry-run +``` + +### Container Management + +```bash +secubox-backup container list # List LXC containers +secubox-backup container backup mitmproxy # Backup specific container +secubox-backup container restore mitmproxy /path/to/backup.tar.gz +secubox-backup container backups mitmproxy # List backups for container +``` + +### Profile Management + +```bash +secubox-backup profile list # List profiles +secubox-backup profile create mysetup # Create from current config +secubox-backup profile apply mysetup # Apply profile +``` + +### Remote Sync (Gitea) + +```bash +secubox-backup sync --push # Push to Gitea +secubox-backup sync --pull # List remote backups +``` + +### Maintenance + +```bash +secubox-backup status # Show backup status +secubox-backup cleanup # Remove old backups (keeps last 10) +``` + +## UCI Configuration + +``` +config backup 'main' + option enabled '1' + option storage_path '/srv/backups' + option retention_days '30' + option max_backups '10' + option compress '1' + +config schedule 'daily' + option enabled '1' + option type 'config' + option cron '0 3 * * *' + +config schedule 'weekly' + option enabled '1' + option type 'full' + option cron '0 4 * * 0' + +config remote 'gitea' + option enabled '1' + option url 'https://git.example.com' + option repo 'user/backups' + option token 'your-token' + option branch 'master' +``` + +## Backup Structure + +``` +/srv/backups/ +├── config/ # UCI and certificate backups +│ └── config-YYYYMMDD-HHMMSS.tar.gz +├── containers/ # LXC container backups +│ ├── mitmproxy-YYYYMMDD-HHMMSS.tar.gz +│ └── haproxy-YYYYMMDD-HHMMSS.tar.gz +├── services/ # Service data backups +│ └── haproxy-YYYYMMDD-HHMMSS.tar.gz +└── profiles/ # Configuration profiles + └── mysetup.json +``` + +## What's Backed Up + +| Type | Contents | +|------|----------| +| **Config** | `/etc/config/*`, `/etc/secubox/*`, `/etc/haproxy/*`, `/srv/haproxy/certs/*`, `/etc/acme/*` | +| **Containers** | Full LXC rootfs from `/srv/lxc//` | +| **Services** | `/srv/haproxy/`, `/srv/mitmproxy/`, `/srv/localai/`, `/srv/gitea/` | + +## Dependencies + +- `lxc` - For container backup/restore +- `tar` - Archive creation +- `wget` - Gitea API communication (optional) diff --git a/package/secubox/secubox-app-backup/files/etc/config/backup b/package/secubox/secubox-app-backup/files/etc/config/backup new file mode 100644 index 00000000..5a26e3c0 --- /dev/null +++ b/package/secubox/secubox-app-backup/files/etc/config/backup @@ -0,0 +1,25 @@ +# SecuBox Unified Backup Manager Configuration + +config backup 'main' + option enabled '1' + option storage_path '/srv/backups' + option retention_days '30' + option max_backups '10' + option compress '1' + +config schedule 'daily' + option enabled '0' + option type 'config' + option cron '0 3 * * *' + +config schedule 'weekly' + option enabled '0' + option type 'full' + option cron '0 4 * * 0' + +config remote 'gitea' + option enabled '0' + option url '' + option repo '' + option token '' + option branch 'master' diff --git a/package/secubox/secubox-app-backup/files/usr/lib/backup/config.sh b/package/secubox/secubox-app-backup/files/usr/lib/backup/config.sh new file mode 100644 index 00000000..7f2f1709 --- /dev/null +++ b/package/secubox/secubox-app-backup/files/usr/lib/backup/config.sh @@ -0,0 +1,132 @@ +#!/bin/sh +# SecuBox Backup - Configuration Backup Functions + +CONFIG_DIRS="/etc/config /etc/secubox /etc/haproxy /etc/mitmproxy" +CERT_DIRS="/srv/haproxy/certs /etc/acme" +PROFILE_DIR="/etc/secubox/profiles" + +# Backup UCI and related configs +config_backup() { + local dest="$1" + local timestamp=$(date +%Y%m%d-%H%M%S) + local backup_file="${dest}/config-${timestamp}.tar.gz" + + echo " Backing up UCI configs..." + + # Create temp dir for staging + local staging="/tmp/backup_staging_$$" + mkdir -p "$staging" + + # Copy config dirs + for dir in $CONFIG_DIRS; do + if [ -d "$dir" ]; then + local reldir=$(dirname "$dir") + mkdir -p "$staging$reldir" + cp -a "$dir" "$staging$reldir/" 2>/dev/null + fi + done + + # Copy certs + for dir in $CERT_DIRS; do + if [ -d "$dir" ]; then + local reldir=$(dirname "$dir") + mkdir -p "$staging$reldir" + cp -a "$dir" "$staging$reldir/" 2>/dev/null + fi + done + + # Create manifest + cat > "$staging/manifest.json" << EOF +{ + "type": "config", + "timestamp": "$(date -Iseconds)", + "hostname": "$(uci -q get system.@system[0].hostname)", + "version": "$(cat /etc/secubox-version 2>/dev/null || echo 'unknown')", + "files": $(find "$staging" -type f | wc -l) +} +EOF + + # Create archive + tar -czf "$backup_file" -C "$staging" . + + # Cleanup + rm -rf "$staging" + + local size=$(du -sh "$backup_file" 2>/dev/null | awk '{print $1}') + echo " Config backup created: $backup_file ($size)" + + return 0 +} + +# Restore config from backup +config_restore() { + local backup_file="$1" + local dry_run="${2:-0}" + + [ -f "$backup_file" ] || { echo "Backup file not found: $backup_file"; return 1; } + + local staging="/tmp/restore_staging_$$" + mkdir -p "$staging" + + echo " Extracting backup..." + tar -xzf "$backup_file" -C "$staging" + + # Show manifest + if [ -f "$staging/manifest.json" ]; then + echo " Backup info:" + cat "$staging/manifest.json" | grep -E "timestamp|hostname|version" | sed 's/[",]//g' | sed 's/^/ /' + fi + + if [ "$dry_run" = "1" ]; then + echo " [DRY RUN] Would restore:" + find "$staging" -type f ! -name "manifest.json" | while read f; do + echo " $f" + done | head -20 + rm -rf "$staging" + return 0 + fi + + # Create safety backup + echo " Creating safety backup..." + config_backup "/tmp" >/dev/null 2>&1 + + # Restore files + echo " Restoring configs..." + for dir in $CONFIG_DIRS $CERT_DIRS; do + local reldir="${dir#/}" + if [ -d "$staging/$reldir" ]; then + cp -a "$staging/$reldir"/* "$dir/" 2>/dev/null + fi + done + + # Cleanup + rm -rf "$staging" + + # Reload services + echo " Reloading services..." + /etc/init.d/network reload 2>/dev/null & + ubus call uci reload 2>/dev/null & + + echo " Config restore complete" + return 0 +} + +# Export specific UCI config +config_export_uci() { + local config="$1" + local dest="$2" + + uci export "$config" > "${dest}/${config}.uci" 2>/dev/null +} + +# List config backups +config_list_backups() { + local backup_dir="$1" + + ls -lh "${backup_dir}/config-"*.tar* 2>/dev/null | while read line; do + local file=$(echo "$line" | awk '{print $NF}') + local size=$(echo "$line" | awk '{print $5}') + local date=$(echo "$line" | awk '{print $6" "$7" "$8}') + printf "%-40s %-10s %s\n" "$(basename "$file")" "$size" "$date" + done +} diff --git a/package/secubox/secubox-app-backup/files/usr/lib/backup/containers.sh b/package/secubox/secubox-app-backup/files/usr/lib/backup/containers.sh new file mode 100644 index 00000000..a7aecbf5 --- /dev/null +++ b/package/secubox/secubox-app-backup/files/usr/lib/backup/containers.sh @@ -0,0 +1,148 @@ +#!/bin/sh +# SecuBox Backup - LXC Container Functions + +LXC_DIR="/srv/lxc" + +# List all LXC containers with status +container_list() { + local json="${1:-0}" + + if [ "$json" = "1" ]; then + printf '[' + local first=1 + for dir in $LXC_DIR/*/; do + [ -d "$dir" ] || continue + local name=$(basename "$dir") + [ -f "$dir/config" ] || continue + + [ $first -eq 0 ] && printf ',' + first=0 + + local state="stopped" + lxc-info -n "$name" 2>/dev/null | grep -q "RUNNING" && state="running" + + local size=$(du -sh "$dir" 2>/dev/null | awk '{print $1}') + + printf '{"name":"%s","state":"%s","size":"%s"}' "$name" "$state" "$size" + done + printf ']' + else + printf "%-20s %-10s %-10s\n" "CONTAINER" "STATE" "SIZE" + printf "%-20s %-10s %-10s\n" "---------" "-----" "----" + + for dir in $LXC_DIR/*/; do + [ -d "$dir" ] || continue + local name=$(basename "$dir") + [ -f "$dir/config" ] || continue + + local state="stopped" + lxc-info -n "$name" 2>/dev/null | grep -q "RUNNING" && state="running" + + local size=$(du -sh "$dir" 2>/dev/null | awk '{print $1}') + + printf "%-20s %-10s %-10s\n" "$name" "$state" "$size" + done + fi +} + +# Backup single container +container_backup() { + local name="$1" + local dest="$2" + local compress="${3:-1}" + + [ -d "$LXC_DIR/$name" ] || { echo "Container not found: $name"; return 1; } + + local was_running=0 + lxc-info -n "$name" 2>/dev/null | grep -q "RUNNING" && was_running=1 + + # Stop container if running + if [ "$was_running" = "1" ]; then + echo " Stopping container..." + lxc-stop -n "$name" -t 30 2>/dev/null + fi + + local timestamp=$(date +%Y%m%d-%H%M%S) + local backup_file="${dest}/${name}-${timestamp}" + + # Create backup + echo " Creating backup..." + if [ "$compress" = "1" ]; then + tar -czf "${backup_file}.tar.gz" -C "$LXC_DIR" "$name" + backup_file="${backup_file}.tar.gz" + else + tar -cf "${backup_file}.tar" -C "$LXC_DIR" "$name" + backup_file="${backup_file}.tar" + fi + + # Save config separately for easy inspection + cp "$LXC_DIR/$name/config" "${dest}/${name}-${timestamp}.config" + + # Restart if was running + if [ "$was_running" = "1" ]; then + echo " Restarting container..." + lxc-start -n "$name" + fi + + local size=$(du -sh "$backup_file" 2>/dev/null | awk '{print $1}') + echo " Backup created: $backup_file ($size)" + + return 0 +} + +# Restore container from backup +container_restore() { + local name="$1" + local backup_file="$2" + + [ -f "$backup_file" ] || { echo "Backup file not found: $backup_file"; return 1; } + + # Stop container if running + if lxc-info -n "$name" 2>/dev/null | grep -q "RUNNING"; then + echo " Stopping container..." + lxc-stop -n "$name" -t 30 2>/dev/null + fi + + # Backup current state first + if [ -d "$LXC_DIR/$name" ]; then + echo " Creating safety backup..." + local safety_backup="${LXC_DIR}/${name}.pre-restore.tar.gz" + tar -czf "$safety_backup" -C "$LXC_DIR" "$name" 2>/dev/null + + echo " Removing old container..." + rm -rf "$LXC_DIR/$name" + fi + + # Extract backup + echo " Extracting backup..." + mkdir -p "$LXC_DIR" + if echo "$backup_file" | grep -q '\.gz$'; then + tar -xzf "$backup_file" -C "$LXC_DIR" + else + tar -xf "$backup_file" -C "$LXC_DIR" + fi + + # Start container + echo " Starting container..." + lxc-start -n "$name" + + local state="stopped" + sleep 2 + lxc-info -n "$name" 2>/dev/null | grep -q "RUNNING" && state="running" + + echo " Container restored: $name ($state)" + return 0 +} + +# List container backups +container_list_backups() { + local name="$1" + local backup_dir="$2" + + ls -lh "${backup_dir}/${name}-"*.tar* 2>/dev/null | while read line; do + local file=$(echo "$line" | awk '{print $NF}') + local size=$(echo "$line" | awk '{print $5}') + local date=$(echo "$line" | awk '{print $6" "$7" "$8}') + printf "%-50s %-10s %s\n" "$(basename "$file")" "$size" "$date" + done +} diff --git a/package/secubox/secubox-app-backup/files/usr/lib/backup/remote.sh b/package/secubox/secubox-app-backup/files/usr/lib/backup/remote.sh new file mode 100644 index 00000000..b6215ad8 --- /dev/null +++ b/package/secubox/secubox-app-backup/files/usr/lib/backup/remote.sh @@ -0,0 +1,139 @@ +#!/bin/sh +# SecuBox Backup - Remote Storage Functions (Gitea/Mesh) + +CONFIG="backup" + +# Get Gitea config +gitea_get_config() { + local url=$(uci -q get $CONFIG.gitea.url) + local repo=$(uci -q get $CONFIG.gitea.repo) + local token=$(uci -q get $CONFIG.gitea.token) + local branch=$(uci -q get $CONFIG.gitea.branch) + + [ -z "$url" ] || [ -z "$repo" ] && return 1 + + echo "$url|$repo|$token|${branch:-master}" +} + +# Push backup to Gitea +gitea_push() { + local backup_file="$1" + local message="${2:-Automated backup}" + + local config=$(gitea_get_config) + [ -z "$config" ] && { echo "Gitea not configured"; return 1; } + + local url=$(echo "$config" | cut -d'|' -f1) + local repo=$(echo "$config" | cut -d'|' -f2) + local token=$(echo "$config" | cut -d'|' -f3) + local branch=$(echo "$config" | cut -d'|' -f4) + + local filename=$(basename "$backup_file") + local content=$(base64 -w 0 < "$backup_file") + + # Check if file exists (to update or create) + local sha="" + local existing=$(wget -q -O - --header="Authorization: token $token" \ + "$url/api/v1/repos/$repo/contents/backups/$filename?ref=$branch" 2>/dev/null) + + if [ -n "$existing" ]; then + sha=$(echo "$existing" | jsonfilter -e '@.sha' 2>/dev/null) + fi + + # Create/update file + local payload + if [ -n "$sha" ]; then + payload="{\"message\":\"$message\",\"content\":\"$content\",\"sha\":\"$sha\",\"branch\":\"$branch\"}" + else + payload="{\"message\":\"$message\",\"content\":\"$content\",\"branch\":\"$branch\"}" + fi + + local response=$(echo "$payload" | wget -q -O - --post-data=- \ + --header="Authorization: token $token" \ + --header="Content-Type: application/json" \ + "$url/api/v1/repos/$repo/contents/backups/$filename" 2>/dev/null) + + if echo "$response" | grep -q '"content"'; then + echo " Pushed to Gitea: $filename" + return 0 + else + echo " Failed to push to Gitea" + return 1 + fi +} + +# Pull backup from Gitea +gitea_pull() { + local filename="$1" + local dest="$2" + + local config=$(gitea_get_config) + [ -z "$config" ] && { echo "Gitea not configured"; return 1; } + + local url=$(echo "$config" | cut -d'|' -f1) + local repo=$(echo "$config" | cut -d'|' -f2) + local token=$(echo "$config" | cut -d'|' -f3) + local branch=$(echo "$config" | cut -d'|' -f4) + + local response=$(wget -q -O - --header="Authorization: token $token" \ + "$url/api/v1/repos/$repo/contents/backups/$filename?ref=$branch" 2>/dev/null) + + if [ -z "$response" ]; then + echo " File not found on Gitea: $filename" + return 1 + fi + + local content=$(echo "$response" | jsonfilter -e '@.content' 2>/dev/null) + + if [ -n "$content" ]; then + echo "$content" | base64 -d > "$dest/$filename" + echo " Downloaded from Gitea: $filename" + return 0 + fi + + return 1 +} + +# List remote backups +gitea_list() { + local config=$(gitea_get_config) + [ -z "$config" ] && { echo "Gitea not configured"; return 1; } + + local url=$(echo "$config" | cut -d'|' -f1) + local repo=$(echo "$config" | cut -d'|' -f2) + local token=$(echo "$config" | cut -d'|' -f3) + local branch=$(echo "$config" | cut -d'|' -f4) + + local response=$(wget -q -O - --header="Authorization: token $token" \ + "$url/api/v1/repos/$repo/contents/backups?ref=$branch" 2>/dev/null) + + if [ -z "$response" ]; then + echo " No remote backups found" + return 0 + fi + + echo "$response" | jsonfilter -e '@[*].name' 2>/dev/null | while read name; do + local size=$(echo "$response" | jsonfilter -e "@[@.name='$name'].size" 2>/dev/null) + printf "%-50s %s\n" "$name" "${size:-?} bytes" + done +} + +# Sync with mesh peers (placeholder) +mesh_sync() { + local mode="${1:-push}" + + if command -v secubox-p2p >/dev/null 2>&1; then + case "$mode" in + push) + echo " Pushing to mesh peers..." + secubox-p2p push-backup 2>/dev/null || echo " Mesh sync not implemented" + ;; + pull) + echo " Pulling from mesh peers..." + secubox-p2p pull-backup 2>/dev/null || echo " Mesh sync not implemented" + ;; + esac + else + echo " Mesh (secubox-p2p) not available" + fi +} diff --git a/package/secubox/secubox-app-backup/files/usr/sbin/secubox-backup b/package/secubox/secubox-app-backup/files/usr/sbin/secubox-backup new file mode 100644 index 00000000..28878c4f --- /dev/null +++ b/package/secubox/secubox-app-backup/files/usr/sbin/secubox-backup @@ -0,0 +1,437 @@ +#!/bin/sh +# SecuBox Unified Backup Manager + +VERSION="1.0.0" +CONFIG="backup" + +# Load libraries +LIB_DIR="/usr/lib/backup" +. "$LIB_DIR/containers.sh" +. "$LIB_DIR/config.sh" +. "$LIB_DIR/remote.sh" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +log() { echo -e "${GREEN}[BACKUP]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1"; } + +get_storage_path() { + local path=$(uci -q get $CONFIG.main.storage_path) + echo "${path:-/srv/backups}" +} + +ensure_storage() { + local path=$(get_storage_path) + mkdir -p "$path/containers" "$path/config" "$path/services" "$path/profiles" +} + +# ============================================================================ +# Create Backup +# ============================================================================ + +cmd_create() { + local type="full" + + while [ $# -gt 0 ]; do + case "$1" in + --full) type="full" ;; + --config) type="config" ;; + --containers) type="containers" ;; + --services) type="services" ;; + *) warn "Unknown option: $1" ;; + esac + shift + done + + ensure_storage + local storage=$(get_storage_path) + local timestamp=$(date +%Y%m%d-%H%M%S) + + log "Creating $type backup..." + + case "$type" in + full) + log "Backing up configurations..." + config_backup "$storage/config" + + log "Backing up containers..." + for dir in /srv/lxc/*/; do + [ -d "$dir" ] || continue + local name=$(basename "$dir") + [ -f "$dir/config" ] || continue + log " Container: $name" + container_backup "$name" "$storage/containers" + done + + log "Backing up service data..." + for svc in haproxy mitmproxy; do + if [ -d "/srv/$svc" ]; then + log " Service: $svc" + tar -czf "$storage/services/${svc}-${timestamp}.tar.gz" -C /srv "$svc" 2>/dev/null + fi + done + ;; + + config) + config_backup "$storage/config" + ;; + + containers) + for dir in /srv/lxc/*/; do + [ -d "$dir" ] || continue + local name=$(basename "$dir") + [ -f "$dir/config" ] || continue + log " Container: $name" + container_backup "$name" "$storage/containers" + done + ;; + + services) + for svc in haproxy mitmproxy localai gitea; do + if [ -d "/srv/$svc" ]; then + log " Service: $svc" + tar -czf "$storage/services/${svc}-${timestamp}.tar.gz" -C /srv "$svc" 2>/dev/null + fi + done + ;; + esac + + log "Backup complete: $storage" + + # Cleanup old backups + cmd_cleanup +} + +# ============================================================================ +# List Backups +# ============================================================================ + +cmd_list() { + local type="all" + + while [ $# -gt 0 ]; do + case "$1" in + --local) type="local" ;; + --remote) type="remote" ;; + --all) type="all" ;; + --json) type="json" ;; + *) warn "Unknown option: $1" ;; + esac + shift + done + + local storage=$(get_storage_path) + + echo "" + echo "===========================================" + echo " SecuBox Backups" + echo "===========================================" + echo "" + echo "Storage: $storage" + echo "" + + if [ "$type" = "local" ] || [ "$type" = "all" ]; then + echo "--- Config Backups ---" + config_list_backups "$storage/config" + echo "" + + echo "--- Container Backups ---" + ls -lh "$storage/containers/"*.tar* 2>/dev/null | awk '{printf "%-50s %-10s %s %s %s\n", $NF, $5, $6, $7, $8}' || echo " None" + echo "" + + echo "--- Service Backups ---" + ls -lh "$storage/services/"*.tar* 2>/dev/null | awk '{printf "%-50s %-10s %s %s %s\n", $NF, $5, $6, $7, $8}' || echo " None" + echo "" + fi + + if [ "$type" = "remote" ] || [ "$type" = "all" ]; then + echo "--- Remote Backups (Gitea) ---" + gitea_list 2>/dev/null || echo " Not configured" + echo "" + fi +} + +# ============================================================================ +# Restore Backup +# ============================================================================ + +cmd_restore() { + local backup_id="$1" + local dry_run=0 + + shift + while [ $# -gt 0 ]; do + case "$1" in + --dry-run) dry_run=1 ;; + *) warn "Unknown option: $1" ;; + esac + shift + done + + if [ -z "$backup_id" ]; then + echo "Usage: secubox-backup restore [--dry-run]" + return 1 + fi + + local storage=$(get_storage_path) + + # Find backup file + local backup_file="" + for dir in "$storage/config" "$storage/containers" "$storage/services"; do + if [ -f "$dir/$backup_id" ]; then + backup_file="$dir/$backup_id" + break + fi + done + + [ -z "$backup_file" ] && { error "Backup not found: $backup_id"; return 1; } + + log "Restoring from: $backup_file" + + # Determine type from path + if echo "$backup_file" | grep -q "/config/"; then + config_restore "$backup_file" "$dry_run" + elif echo "$backup_file" | grep -q "/containers/"; then + local name=$(basename "$backup_file" | sed 's/-[0-9]*-.*//') + container_restore "$name" "$backup_file" + else + error "Unknown backup type" + return 1 + fi +} + +# ============================================================================ +# Container Commands +# ============================================================================ + +cmd_container() { + local action="$1" + shift + + case "$action" in + list) + container_list "${1:-0}" + ;; + + backup) + local name="$1" + [ -z "$name" ] && { echo "Usage: secubox-backup container backup "; return 1; } + ensure_storage + local storage=$(get_storage_path) + log "Backing up container: $name" + container_backup "$name" "$storage/containers" + ;; + + restore) + local name="$1" + local backup="$2" + [ -z "$name" ] || [ -z "$backup" ] && { echo "Usage: secubox-backup container restore "; return 1; } + log "Restoring container: $name" + container_restore "$name" "$backup" + ;; + + backups) + local name="$1" + [ -z "$name" ] && { echo "Usage: secubox-backup container backups "; return 1; } + local storage=$(get_storage_path) + container_list_backups "$name" "$storage/containers" + ;; + + *) + echo "Container commands:" + echo " list List containers" + echo " backup Backup container" + echo " restore Restore container from backup" + echo " backups List backups for container" + ;; + esac +} + +# ============================================================================ +# Profile Commands +# ============================================================================ + +cmd_profile() { + local action="$1" + shift + + # Delegate to secubox-profile if available + if command -v secubox-profile >/dev/null 2>&1; then + case "$action" in + list) secubox-profile list "$@" ;; + create) secubox-profile export "$@" ;; + apply) secubox-profile apply "$@" ;; + share) secubox-profile share "$@" ;; + *) + echo "Profile commands (via secubox-profile):" + echo " list List profiles" + echo " create Create profile from current config" + echo " apply Apply profile" + echo " share Share profile" + ;; + esac + else + echo "secubox-profile not installed" + fi +} + +# ============================================================================ +# Sync Commands +# ============================================================================ + +cmd_sync() { + local mode="push" + + while [ $# -gt 0 ]; do + case "$1" in + --push) mode="push" ;; + --pull) mode="pull" ;; + *) warn "Unknown option: $1" ;; + esac + shift + done + + local storage=$(get_storage_path) + + case "$mode" in + push) + log "Pushing backups to remote..." + + # Push latest config backup + local latest=$(ls -t "$storage/config/"*.tar.gz 2>/dev/null | head -1) + [ -n "$latest" ] && gitea_push "$latest" "Config backup $(date +%Y-%m-%d)" + ;; + + pull) + log "Pulling backups from remote..." + gitea_list + ;; + esac +} + +# ============================================================================ +# Cleanup +# ============================================================================ + +cmd_cleanup() { + local storage=$(get_storage_path) + local max=$(uci -q get $CONFIG.main.max_backups) + max=${max:-10} + + log "Cleaning up old backups (keeping last $max)..." + + for dir in config containers services; do + local count=$(ls -1 "$storage/$dir/"*.tar* 2>/dev/null | wc -l) + if [ "$count" -gt "$max" ]; then + ls -1t "$storage/$dir/"*.tar* 2>/dev/null | tail -n +$((max + 1)) | while read f; do + rm -f "$f" + log " Removed: $(basename "$f")" + done + fi + done +} + +# ============================================================================ +# Status +# ============================================================================ + +cmd_status() { + local storage=$(get_storage_path) + + echo "" + echo "===========================================" + echo " SecuBox Backup Status" + echo "===========================================" + echo "" + echo "Storage Path: $storage" + echo "Storage Used: $(du -sh "$storage" 2>/dev/null | awk '{print $1}')" + echo "" + + echo "Last Backups:" + for type in config containers services; do + local latest=$(ls -t "$storage/$type/"*.tar* 2>/dev/null | head -1) + if [ -n "$latest" ]; then + local date=$(stat -c %y "$latest" 2>/dev/null | cut -d' ' -f1,2 | cut -d'.' -f1) + printf " %-12s %s\n" "$type:" "$date" + else + printf " %-12s %s\n" "$type:" "none" + fi + done + + echo "" + echo "Containers:" + for dir in /srv/lxc/*/; do + [ -d "$dir" ] || continue + local name=$(basename "$dir") + [ -f "$dir/config" ] || continue + local state="stopped" + lxc-info -n "$name" 2>/dev/null | grep -q "RUNNING" && state="running" + printf " %-20s %s\n" "$name" "$state" + done + echo "" +} + +# ============================================================================ +# Help +# ============================================================================ + +show_help() { + cat << EOF +SecuBox Unified Backup Manager v${VERSION} + +Usage: secubox-backup [options] + +Commands: + create [--full|--config|--containers|--services] + Create backup + list [--local|--remote|--all] + List backups + restore [--dry-run] + Restore from backup + status Show backup status + cleanup Remove old backups + + container list List LXC containers + container backup Backup container + container restore + Restore container + + profile list List profiles + profile create Create profile + profile apply Apply profile + + sync [--push|--pull] Sync with remote (Gitea) + +Examples: + secubox-backup create --full + secubox-backup container backup mitmproxy + secubox-backup container restore mitmproxy /srv/backups/containers/mitmproxy-20260205.tar.gz + secubox-backup list + secubox-backup sync --push + +EOF +} + +# ============================================================================ +# Main +# ============================================================================ + +case "${1:-}" in + create) shift; cmd_create "$@" ;; + list|ls) shift; cmd_list "$@" ;; + restore) shift; cmd_restore "$@" ;; + status) shift; cmd_status "$@" ;; + cleanup) shift; cmd_cleanup "$@" ;; + container) shift; cmd_container "$@" ;; + profile) shift; cmd_profile "$@" ;; + sync) shift; cmd_sync "$@" ;; + help|--help|-h|'') show_help ;; + *) error "Unknown command: $1"; show_help >&2; exit 1 ;; +esac + +exit 0 diff --git a/package/secubox/secubox-app-dns-provider/README.md b/package/secubox/secubox-app-dns-provider/README.md index 77681771..10618fb3 100644 --- a/package/secubox/secubox-app-dns-provider/README.md +++ b/package/secubox/secubox-app-dns-provider/README.md @@ -47,15 +47,56 @@ Each adapter in `/usr/lib/secubox/dns/` implements: ## CLI Usage +### Basic Operations + ```bash dnsctl status # Show config status dnsctl test # Verify API credentials dnsctl list # List zone records dnsctl add A myservice 1.2.3.4 # Create A record dnsctl add CNAME www mycdn.net # Create CNAME +dnsctl update A myservice 5.6.7.8 # Update existing record +dnsctl get A www # Get record value dnsctl rm A myservice # Remove record -dnsctl sync # Sync HAProxy vhosts to DNS -dnsctl verify myservice.example.com # Check propagation +dnsctl domains # List all domains in account +``` + +### HAProxy Sync + +```bash +dnsctl sync # Sync HAProxy vhosts to DNS A records +dnsctl verify myservice.example.com # Check propagation (1.1.1.1, 8.8.8.8, 9.9.9.9) +``` + +### Subdomain Generator + +```bash +dnsctl generate gitea # Auto-create gitea.zone with public IP +dnsctl generate api prod # Create prod-api.zone +dnsctl suggest web # Show subdomain name suggestions +dnsctl suggest mail # Suggestions: mail, smtp, imap, webmail, mx +dnsctl suggest dev # Suggestions: git, dev, staging, test, ci +``` + +### DynDNS + +```bash +dnsctl dyndns # Update root A record with WAN IP +dnsctl dyndns api 300 # Update api.zone with 5min TTL +``` + +### Mail DNS Setup + +```bash +dnsctl mail-setup # Create MX, SPF, DMARC records +dnsctl mail-setup mail 10 # Custom hostname and priority +dnsctl dkim-add mail '' # Add DKIM TXT record +``` + +### SSL Certificates + +```bash +dnsctl acme-dns01 example.com # Issue cert via DNS-01 challenge dnsctl acme-dns01 '*.example.com' # Wildcard cert via DNS-01 ``` diff --git a/package/secubox/secubox-app-dns-provider/files/usr/sbin/dnsctl b/package/secubox/secubox-app-dns-provider/files/usr/sbin/dnsctl index 561f7606..d936deb6 100644 --- a/package/secubox/secubox-app-dns-provider/files/usr/sbin/dnsctl +++ b/package/secubox/secubox-app-dns-provider/files/usr/sbin/dnsctl @@ -317,6 +317,170 @@ cmd_domains() { fi } +# ============================================================================ +# Subdomain Generator +# ============================================================================ + +cmd_generate() { + local service="$1" + local prefix="${2:-}" + + if [ -z "$service" ]; then + echo "Usage: dnsctl generate [prefix]" + echo " Auto-generates unique subdomain and creates A record" + echo "" + echo "Examples:" + echo " dnsctl generate gitea # Creates gitea.zone.tld" + echo " dnsctl generate blog dev # Creates dev-blog.zone.tld" + echo " dnsctl generate api prod # Creates prod-api.zone.tld" + return 1 + fi + + load_provider + local zone=$(get_zone) + local public_ip=$(curl -s --connect-timeout 5 https://ipv4.icanhazip.com 2>/dev/null | tr -d '\n') + + [ -z "$public_ip" ] && { error "Cannot detect public IP"; return 1; } + + # Build subdomain name + local subdomain + if [ -n "$prefix" ]; then + subdomain="${prefix}-${service}" + else + subdomain="$service" + fi + + # Check if subdomain exists + local existing=$(nslookup "${subdomain}.${zone}" 8.8.8.8 2>/dev/null | grep "Address:" | tail -1) + if echo "$existing" | grep -q "[0-9]"; then + warn "Subdomain ${subdomain}.${zone} already exists" + # Try with numeric suffix + local i=2 + while [ $i -lt 10 ]; do + local try_sub="${subdomain}${i}" + local try_check=$(nslookup "${try_sub}.${zone}" 8.8.8.8 2>/dev/null | grep "Address:" | tail -1) + if ! echo "$try_check" | grep -q "[0-9]"; then + subdomain="$try_sub" + break + fi + i=$((i + 1)) + done + fi + + log "Generating subdomain: ${subdomain}.${zone}" + dns_add "$zone" "A" "$subdomain" "$public_ip" 3600 + + echo "" + echo "Created: ${subdomain}.${zone} → $public_ip" + echo "" + log "Verify with: dnsctl verify ${subdomain}.${zone}" +} + +cmd_suggest() { + local category="${1:-web}" + + echo "" + echo "Subdomain suggestions for category: $category" + echo "" + + case "$category" in + web) + echo " www, blog, shop, app, portal, admin, api" + ;; + mail) + echo " mail, smtp, imap, pop, webmail, mx, mta" + ;; + dev) + echo " git, dev, staging, test, ci, cd, build" + ;; + media) + echo " media, cdn, stream, video, music, files" + ;; + iot) + echo " mqtt, home, sensor, hub, iot, zwave, zigbee" + ;; + security) + echo " vpn, tor, proxy, guard, auth, sso" + ;; + *) + echo " Custom: use 'dnsctl generate '" + ;; + esac + echo "" +} + +# ============================================================================ +# Mail DNS Records (MX, SPF, DKIM, DMARC) +# ============================================================================ + +cmd_mail_setup() { + local mail_host="${1:-mail}" + local priority="${2:-10}" + + load_provider + local zone=$(get_zone) + local public_ip=$(curl -s --connect-timeout 5 https://ipv4.icanhazip.com 2>/dev/null | tr -d '\n') + + [ -z "$public_ip" ] && { error "Cannot detect public IP"; return 1; } + + log "Setting up mail DNS records for $zone..." + + # Create mail A record + log " Creating A record: ${mail_host}.${zone} → $public_ip" + dns_add "$zone" "A" "$mail_host" "$public_ip" 3600 + + # Create MX record + log " Creating MX record: ${zone} → ${mail_host}.${zone} (priority $priority)" + dns_add "$zone" "MX" "@" "${priority} ${mail_host}.${zone}." 3600 + + # Create SPF record + local spf="v=spf1 mx a:${mail_host}.${zone} ~all" + log " Creating SPF record: $spf" + dns_add "$zone" "TXT" "@" "$spf" 3600 + + # Create DMARC record (relaxed policy for start) + local dmarc="v=DMARC1; p=none; rua=mailto:postmaster@${zone}" + log " Creating DMARC record: $dmarc" + dns_add "$zone" "TXT" "_dmarc" "$dmarc" 3600 + + echo "" + log "Mail DNS setup complete!" + echo "" + echo "Next steps:" + echo " 1. Configure your mail server at ${mail_host}.${zone}" + echo " 2. Generate DKIM keys and add TXT record:" + echo " dnsctl add TXT mail._domainkey ''" + echo " 3. Verify MX: dig MX ${zone}" + echo " 4. Test SPF: dig TXT ${zone}" + echo "" +} + +cmd_dkim_add() { + local selector="${1:-mail}" + local public_key="$2" + + if [ -z "$public_key" ]; then + echo "Usage: dnsctl dkim-add [selector] " + echo " selector defaults to 'mail'" + echo "" + echo "Generate DKIM key pair:" + echo " openssl genrsa -out dkim.private 2048" + echo " openssl rsa -in dkim.private -pubout -out dkim.public" + echo " # Use content between -----BEGIN/END PUBLIC KEY----- (no newlines)" + return 1 + fi + + load_provider + local zone=$(get_zone) + + # Format DKIM record + local dkim="v=DKIM1; k=rsa; p=${public_key}" + log "Adding DKIM record: ${selector}._domainkey.${zone}" + dns_add "$zone" "TXT" "${selector}._domainkey" "$dkim" 3600 + + log "DKIM record added. Verify with: dig TXT ${selector}._domainkey.${zone}" +} + # ============================================================================ # ACME DNS-01 Helper # ============================================================================ @@ -384,6 +548,14 @@ Commands: DynDNS: dyndns [subdomain] [ttl] Update A record with current WAN IP +Generator: + generate [prefix] Auto-generate subdomain with A record + suggest [category] Show subdomain name suggestions + +Mail: + mail-setup [host] [priority] Set up MX, SPF, DMARC records + dkim-add [selector] Add DKIM TXT record + SSL: acme-dns01 Issue SSL cert via DNS-01 challenge @@ -413,6 +585,10 @@ case "${1:-}" in status) shift; cmd_status "$@" ;; domains) shift; cmd_domains "$@" ;; dyndns) shift; cmd_dyndns "$@" ;; + generate) shift; cmd_generate "$@" ;; + suggest) shift; cmd_suggest "$@" ;; + mail-setup) shift; cmd_mail_setup "$@" ;; + dkim-add) shift; cmd_dkim_add "$@" ;; acme-dns01) shift; cmd_acme_dns01 "$@" ;; help|--help|-h|'') show_help ;; *) error "Unknown command: $1"; show_help >&2; exit 1 ;; diff --git a/package/secubox/secubox-app-mailserver/Makefile b/package/secubox/secubox-app-mailserver/Makefile new file mode 100644 index 00000000..60499900 --- /dev/null +++ b/package/secubox/secubox-app-mailserver/Makefile @@ -0,0 +1,39 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=secubox-app-mailserver +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=SecuBox Team +PKG_LICENSE:=MIT + +include $(INCLUDE_DIR)/package.mk + +define Package/$(PKG_NAME) + SECTION:=secubox + CATEGORY:=SecuBox + TITLE:=SecuBox Mail Server Manager + DEPENDS:=+lxc +secubox-app-dns-provider + PKGARCH:=all +endef + +define Package/$(PKG_NAME)/description + Custom mail server (Postfix + Dovecot) in LXC with mesh backup support. + Integrates with dnsctl for MX/SPF/DKIM/DMARC management. +endef + +define Package/$(PKG_NAME)/install + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) ./files/etc/config/mailserver $(1)/etc/config/ + + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./files/etc/init.d/mailserver $(1)/etc/init.d/ + + $(INSTALL_DIR) $(1)/usr/sbin + $(INSTALL_BIN) ./files/usr/sbin/mailctl $(1)/usr/sbin/ + + $(INSTALL_DIR) $(1)/usr/lib/mailserver + $(INSTALL_DATA) ./files/usr/lib/mailserver/*.sh $(1)/usr/lib/mailserver/ +endef + +$(eval $(call BuildPackage,$(PKG_NAME))) diff --git a/package/secubox/secubox-app-mailserver/README.md b/package/secubox/secubox-app-mailserver/README.md new file mode 100644 index 00000000..eaa5676e --- /dev/null +++ b/package/secubox/secubox-app-mailserver/README.md @@ -0,0 +1,208 @@ +# SecuBox Mail Server + +Custom mail server (Postfix + Dovecot) running in LXC container with mesh backup support. + +## Features + +- **Postfix** - SMTP server with virtual domains +- **Dovecot** - IMAP/POP3 with LMTP delivery +- **Rspamd** - Spam filtering +- **OpenDKIM** - Email signing +- **Mesh Backup** - P2P backup sync with other SecuBox nodes +- **Webmail Integration** - Works with Roundcube container + +## Installation + +```bash +opkg install secubox-app-mailserver +``` + +## Quick Start + +```bash +# 1. Configure domain +uci set mailserver.main.domain='example.com' +uci set mailserver.main.hostname='mail' +uci commit mailserver + +# 2. Install container +mailctl install + +# 3. Set up DNS records (MX, SPF, DMARC) +mailctl dns-setup + +# 4. Get SSL certificate +mailctl ssl-setup + +# 5. Add mail users +mailctl user add user@example.com + +# 6. Enable and start +uci set mailserver.main.enabled=1 +uci commit mailserver +/etc/init.d/mailserver start + +# 7. Configure webmail +mailctl webmail configure +``` + +## CLI Reference + +### Service Control + +```bash +mailctl start # Start mail server +mailctl stop # Stop mail server +mailctl restart # Restart mail server +mailctl status # Show status +``` + +### User Management + +```bash +mailctl user add user@domain.com # Add user (prompts for password) +mailctl user del user@domain.com # Delete user +mailctl user list # List all users +mailctl user passwd user@domain.com # Change password +``` + +### Aliases + +```bash +mailctl alias add info@domain.com user@domain.com +mailctl alias list +``` + +### SSL Certificates + +```bash +mailctl ssl-setup # Obtain Let's Encrypt cert via DNS-01 +mailctl ssl-status # Show certificate info +``` + +### DNS Integration + +```bash +mailctl dns-setup # Create MX, SPF, DMARC records via dnsctl +``` + +### Mesh Backup + +```bash +mailctl mesh backup # Create backup for mesh sync +mailctl mesh restore # Restore from backup +mailctl mesh sync push # Push to mesh peers +mailctl mesh sync pull # Pull from mesh peers +mailctl mesh add-peer # Add mesh peer +mailctl mesh peers # List configured peers +mailctl mesh enable # Enable mesh sync +mailctl mesh disable # Disable mesh sync +``` + +### Webmail + +```bash +mailctl webmail status # Check Roundcube status +mailctl webmail configure # Point Roundcube to this server +``` + +### Diagnostics + +```bash +mailctl logs # View mail logs (last 50 lines) +mailctl logs 100 # View last 100 lines +mailctl test user@external.com # Send test email +``` + +## UCI Configuration + +``` +config mailserver 'main' + option enabled '0' + option hostname 'mail' + option domain 'example.com' + option postmaster 'postmaster' + option data_path '/srv/mailserver' + option container 'mailserver' + +config ports 'ports' + option smtp '25' + option submission '587' + option smtps '465' + option imap '143' + option imaps '993' + option pop3 '110' + option pop3s '995' + +config features 'features' + option spam_filter '1' + option virus_scan '0' + option dkim '1' + option spf '1' + option dmarc '1' + option fail2ban '1' + +config ssl 'ssl' + option type 'letsencrypt' + +config webmail 'webmail' + option enabled '1' + option container 'secubox-webmail' + option port '8026' + +config mesh 'mesh' + option enabled '0' + option backup_peers '' + option sync_interval '3600' +``` + +## Data Structure + +``` +/srv/mailserver/ +├── config/ # Postfix/Dovecot config +│ ├── vmailbox # Virtual mailbox map +│ ├── valias # Virtual alias map +│ └── users # Dovecot user database +├── mail/ # Maildir storage +│ └── example.com/ +│ └── user/ +│ ├── cur/ +│ ├── new/ +│ └── tmp/ +└── ssl/ # SSL certificates + ├── fullchain.pem + └── privkey.pem +``` + +## DNS Records Required + +The `mailctl dns-setup` command creates these records via `dnsctl`: + +| Type | Name | Value | +|------|------|-------| +| A | mail | `` | +| MX | @ | `10 mail.example.com.` | +| TXT | @ | `v=spf1 mx a:mail.example.com ~all` | +| TXT | _dmarc | `v=DMARC1; p=none; rua=mailto:postmaster@example.com` | +| TXT | mail._domainkey | `v=DKIM1; k=rsa; p=` | + +## Ports + +| Port | Protocol | Description | +|------|----------|-------------| +| 25 | SMTP | Mail transfer (server-to-server) | +| 587 | Submission | Mail submission (client-to-server) | +| 465 | SMTPS | Secure SMTP | +| 143 | IMAP | Mail access | +| 993 | IMAPS | Secure IMAP | +| 110 | POP3 | Mail download (optional) | +| 995 | POP3S | Secure POP3 (optional) | +| 4190 | Sieve | Mail filtering rules | + +## Dependencies + +- `lxc` - Container runtime +- `secubox-app-dns-provider` - DNS record management +- `acme` - SSL certificate automation (optional) +- `secubox-p2p` - Mesh backup sync (optional) diff --git a/package/secubox/secubox-app-mailserver/files/etc/config/mailserver b/package/secubox/secubox-app-mailserver/files/etc/config/mailserver new file mode 100644 index 00000000..a1c19a74 --- /dev/null +++ b/package/secubox/secubox-app-mailserver/files/etc/config/mailserver @@ -0,0 +1,44 @@ +# SecuBox Mail Server Configuration + +config mailserver 'main' + option enabled '0' + option hostname 'mail' + option domain 'example.com' + option postmaster 'postmaster' + option data_path '/srv/mailserver' + option container 'mailserver' + +config ports 'ports' + option smtp '25' + option submission '587' + option smtps '465' + option imap '143' + option imaps '993' + option pop3 '110' + option pop3s '995' + option sieve '4190' + +config features 'features' + option spam_filter '1' + option virus_scan '0' + option dkim '1' + option spf '1' + option dmarc '1' + option fail2ban '1' + option quota '0' + option quota_default '1G' + +config ssl 'ssl' + option type 'letsencrypt' + option cert_path '' + option key_path '' + +config webmail 'webmail' + option enabled '1' + option container 'secubox-webmail' + option port '8026' + +config mesh 'mesh' + option enabled '0' + option backup_peers '' + option sync_interval '3600' diff --git a/package/secubox/secubox-app-mailserver/files/etc/init.d/mailserver b/package/secubox/secubox-app-mailserver/files/etc/init.d/mailserver new file mode 100644 index 00000000..f51bf4b2 --- /dev/null +++ b/package/secubox/secubox-app-mailserver/files/etc/init.d/mailserver @@ -0,0 +1,59 @@ +#!/bin/sh /etc/rc.common + +START=95 +STOP=10 +USE_PROCD=1 + +NAME="mailserver" + +start_service() { + local enabled=$(uci -q get mailserver.main.enabled) + [ "$enabled" = "1" ] || { + echo "Mail server is disabled. Enable with: uci set mailserver.main.enabled=1" + return 0 + } + + local container=$(uci -q get mailserver.main.container) + container="${container:-mailserver}" + + if ! lxc-info -n "$container" >/dev/null 2>&1; then + echo "Container '$container' not found. Create with: mailctl install" + return 1 + fi + + echo "Starting mail server container: $container" + lxc-start -n "$container" +} + +stop_service() { + local container=$(uci -q get mailserver.main.container) + container="${container:-mailserver}" + + if lxc-info -n "$container" 2>/dev/null | grep -q "RUNNING"; then + echo "Stopping mail server container: $container" + lxc-stop -n "$container" -t 30 + fi +} + +reload_service() { + local container=$(uci -q get mailserver.main.container) + container="${container:-mailserver}" + + if lxc-info -n "$container" 2>/dev/null | grep -q "RUNNING"; then + echo "Reloading mail server services..." + lxc-attach -n "$container" -- postfix reload 2>/dev/null + lxc-attach -n "$container" -- doveadm reload 2>/dev/null + fi +} + +status() { + local container=$(uci -q get mailserver.main.container) + container="${container:-mailserver}" + + if lxc-info -n "$container" 2>/dev/null | grep -q "RUNNING"; then + echo "Mail server: Running" + lxc-attach -n "$container" -- postfix status 2>/dev/null + else + echo "Mail server: Stopped" + fi +} diff --git a/package/secubox/secubox-app-mailserver/files/usr/lib/mailserver/container.sh b/package/secubox/secubox-app-mailserver/files/usr/lib/mailserver/container.sh new file mode 100644 index 00000000..aff52eb5 --- /dev/null +++ b/package/secubox/secubox-app-mailserver/files/usr/lib/mailserver/container.sh @@ -0,0 +1,133 @@ +#!/bin/sh +# Mail Server Container Management + +LXC_DIR="/srv/lxc" +ALPINE_MIRROR="http://dl-cdn.alpinelinux.org/alpine" +ALPINE_VERSION="v3.19" + +# Create mail server LXC container with Postfix + Dovecot +container_create() { + local name="$1" + local data_path="$2" + + [ -d "$LXC_DIR/$name" ] && { echo "Container $name already exists"; return 1; } + + echo "Creating mail server container: $name" + + # Create directory structure + mkdir -p "$LXC_DIR/$name/rootfs" + mkdir -p "$data_path/mail" "$data_path/config" "$data_path/ssl" + + # Create LXC config + cat > "$LXC_DIR/$name/config" << EOF +lxc.uts.name = $name +lxc.rootfs.path = dir:$LXC_DIR/$name/rootfs +lxc.net.0.type = veth +lxc.net.0.link = br-lan +lxc.net.0.flags = up +lxc.net.0.ipv4.address = auto + +lxc.cgroup2.memory.max = 536870912 +lxc.seccomp.profile = + +lxc.mount.entry = $data_path/mail var/mail none bind,create=dir 0 0 +lxc.mount.entry = $data_path/config etc/postfix none bind,create=dir 0 0 +lxc.mount.entry = $data_path/ssl etc/ssl/mail none bind,create=dir 0 0 + +lxc.autodev = 1 +lxc.pty.max = 1024 +EOF + + # Bootstrap Alpine rootfs + echo "Bootstrapping Alpine rootfs..." + local rootfs="$LXC_DIR/$name/rootfs" + + mkdir -p "$rootfs"/{dev,proc,sys,tmp,var/log,var/mail,etc/{postfix,dovecot,ssl/mail}} + + # Download and extract Alpine minirootfs + local arch="aarch64" + local tarball="alpine-minirootfs-3.19.1-${arch}.tar.gz" + local url="$ALPINE_MIRROR/$ALPINE_VERSION/releases/$arch/$tarball" + + echo "Downloading Alpine minirootfs..." + wget -q -O "/tmp/$tarball" "$url" || { echo "Download failed"; return 1; } + + echo "Extracting..." + tar -xzf "/tmp/$tarball" -C "$rootfs" + rm -f "/tmp/$tarball" + + # Configure Alpine + cat > "$rootfs/etc/resolv.conf" << EOF +nameserver 192.168.255.1 +nameserver 1.1.1.1 +EOF + + cat > "$rootfs/etc/apk/repositories" << EOF +$ALPINE_MIRROR/$ALPINE_VERSION/main +$ALPINE_MIRROR/$ALPINE_VERSION/community +EOF + + # Install script for first boot + cat > "$rootfs/root/setup.sh" << 'SETUP' +#!/bin/sh +apk update +apk add postfix postfix-pcre dovecot dovecot-lmtpd dovecot-pigeonhole-plugin rspamd opendkim +# Configure postfix +postconf -e 'myhostname = MAIL_HOSTNAME' +postconf -e 'mydomain = MAIL_DOMAIN' +postconf -e 'mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain' +postconf -e 'inet_interfaces = all' +postconf -e 'home_mailbox = Maildir/' +postconf -e 'smtpd_sasl_type = dovecot' +postconf -e 'smtpd_sasl_path = private/auth' +postconf -e 'smtpd_sasl_auth_enable = yes' +postconf -e 'smtpd_tls_cert_file = /etc/ssl/mail/fullchain.pem' +postconf -e 'smtpd_tls_key_file = /etc/ssl/mail/privkey.pem' +postconf -e 'smtpd_tls_security_level = may' +postconf -e 'smtp_tls_security_level = may' +postconf -e 'virtual_mailbox_domains = /etc/postfix/vdomains' +postconf -e 'virtual_mailbox_maps = hash:/etc/postfix/vmailbox' +postconf -e 'virtual_alias_maps = hash:/etc/postfix/valias' +postconf -e 'virtual_mailbox_base = /var/mail' +postconf -e 'virtual_uid_maps = static:1000' +postconf -e 'virtual_gid_maps = static:1000' +# Create vmail user +addgroup -g 1000 vmail +adduser -D -u 1000 -G vmail -h /var/mail vmail +# Enable services +rc-update add postfix default +rc-update add dovecot default +rc-update add rspamd default +echo "Setup complete" +SETUP + chmod +x "$rootfs/root/setup.sh" + + echo "Container created. Start with: lxc-start -n $name" + return 0 +} + +# Start container +container_start() { + local name="$1" + [ -d "$LXC_DIR/$name" ] || { echo "Container $name not found"; return 1; } + lxc-start -n "$name" +} + +# Stop container +container_stop() { + local name="$1" + lxc-stop -n "$name" -t 30 +} + +# Execute command in container +container_exec() { + local name="$1" + shift + lxc-attach -n "$name" -- "$@" +} + +# Check container status +container_status() { + local name="$1" + lxc-info -n "$name" 2>/dev/null +} diff --git a/package/secubox/secubox-app-mailserver/files/usr/lib/mailserver/mesh.sh b/package/secubox/secubox-app-mailserver/files/usr/lib/mailserver/mesh.sh new file mode 100644 index 00000000..2062a1e6 --- /dev/null +++ b/package/secubox/secubox-app-mailserver/files/usr/lib/mailserver/mesh.sh @@ -0,0 +1,115 @@ +#!/bin/sh +# Mail Server Mesh Backup & Sync + +CONFIG="mailserver" + +get_data_path() { + uci -q get $CONFIG.main.data_path || echo "/srv/mailserver" +} + +# Backup mail data for mesh sync +mesh_backup() { + local data_path=$(get_data_path) + local backup_dir="/srv/backups/mailserver" + local timestamp=$(date +%Y%m%d-%H%M%S) + + mkdir -p "$backup_dir" + + echo "Creating mail backup for mesh sync..." + + # Backup config (small, always sync) + tar -czf "$backup_dir/config-${timestamp}.tar.gz" \ + -C "$data_path" config 2>/dev/null + + # Backup mail data (larger, selective sync) + tar -czf "$backup_dir/mail-${timestamp}.tar.gz" \ + -C "$data_path" mail 2>/dev/null + + echo "Backup created: $backup_dir/*-${timestamp}.tar.gz" + + # Push to mesh if secubox-p2p available + if command -v secubox-p2p >/dev/null 2>&1; then + local mesh_enabled=$(uci -q get $CONFIG.mesh.enabled) + if [ "$mesh_enabled" = "1" ]; then + echo "Pushing to mesh peers..." + secubox-p2p publish "mailbackup:config:$backup_dir/config-${timestamp}.tar.gz" + fi + fi +} + +# Restore from mesh backup +mesh_restore() { + local backup_file="$1" + local data_path=$(get_data_path) + + [ -f "$backup_file" ] || { echo "Backup not found: $backup_file"; return 1; } + + echo "Restoring from: $backup_file" + + # Determine type from filename + if echo "$backup_file" | grep -q "config-"; then + tar -xzf "$backup_file" -C "$data_path" + elif echo "$backup_file" | grep -q "mail-"; then + tar -xzf "$backup_file" -C "$data_path" + fi + + echo "Restore complete. Restart mail server to apply." +} + +# Sync with mesh peers +mesh_sync() { + local mode="${1:-pull}" + + if ! command -v secubox-p2p >/dev/null 2>&1; then + echo "Mesh sync requires secubox-p2p" + return 1 + fi + + local mesh_enabled=$(uci -q get $CONFIG.mesh.enabled) + [ "$mesh_enabled" = "1" ] || { echo "Mesh sync disabled"; return 1; } + + case "$mode" in + push) + mesh_backup + ;; + pull) + echo "Checking mesh for mail backups..." + secubox-p2p list | grep "mailbackup:" | while read entry; do + local file=$(echo "$entry" | cut -d: -f3) + echo " Found: $file" + done + ;; + *) + echo "Usage: mesh_sync [push|pull]" + ;; + esac +} + +# Configure mesh peers +mesh_add_peer() { + local peer="$1" + [ -z "$peer" ] && { echo "Usage: mesh_add_peer "; return 1; } + + local peers=$(uci -q get $CONFIG.mesh.backup_peers) + if [ -z "$peers" ]; then + uci set $CONFIG.mesh.backup_peers="$peer" + else + uci set $CONFIG.mesh.backup_peers="$peers $peer" + fi + uci commit $CONFIG + + echo "Mesh peer added: $peer" +} + +# List mesh peers +mesh_list_peers() { + local peers=$(uci -q get $CONFIG.mesh.backup_peers) + if [ -n "$peers" ]; then + echo "Mesh Backup Peers:" + for peer in $peers; do + echo " $peer" + done + else + echo "No mesh peers configured" + fi +} diff --git a/package/secubox/secubox-app-mailserver/files/usr/lib/mailserver/users.sh b/package/secubox/secubox-app-mailserver/files/usr/lib/mailserver/users.sh new file mode 100644 index 00000000..32abc353 --- /dev/null +++ b/package/secubox/secubox-app-mailserver/files/usr/lib/mailserver/users.sh @@ -0,0 +1,159 @@ +#!/bin/sh +# Mail Server User Management + +CONFIG="mailserver" + +get_container() { + uci -q get $CONFIG.main.container || echo "mailserver" +} + +get_data_path() { + uci -q get $CONFIG.main.data_path || echo "/srv/mailserver" +} + +# Add mail user +user_add() { + local email="$1" + local password="$2" + + [ -z "$email" ] && { echo "Usage: user_add "; return 1; } + + local user=$(echo "$email" | cut -d@ -f1) + local domain=$(echo "$email" | cut -d@ -f2) + local container=$(get_container) + local data_path=$(get_data_path) + + # Validate + echo "$email" | grep -qE '^[^@]+@[^@]+\.[^@]+$' || { echo "Invalid email format"; return 1; } + + # Create mailbox directory + local maildir="$data_path/mail/$domain/$user" + mkdir -p "$maildir"/{cur,new,tmp} + chown -R 1000:1000 "$maildir" + + # Add to virtual mailbox map + local vmailbox="$data_path/config/vmailbox" + touch "$vmailbox" + grep -q "^$email" "$vmailbox" || echo "$email $domain/$user/" >> "$vmailbox" + + # Generate password hash + if [ -z "$password" ]; then + echo "Enter password for $email:" + read -s password + fi + + # Add to dovecot users + local passfile="$data_path/config/users" + local hash=$(lxc-attach -n "$container" -- doveadm pw -s SHA512-CRYPT -p "$password" 2>/dev/null) + [ -z "$hash" ] && hash=$(openssl passwd -6 "$password") + + grep -v "^$email:" "$passfile" > "${passfile}.tmp" 2>/dev/null + echo "$email:$hash" >> "${passfile}.tmp" + mv "${passfile}.tmp" "$passfile" + + # Postmap + lxc-attach -n "$container" -- postmap /etc/postfix/vmailbox 2>/dev/null + + echo "User added: $email" +} + +# Delete mail user +user_del() { + local email="$1" + [ -z "$email" ] && { echo "Usage: user_del "; return 1; } + + local container=$(get_container) + local data_path=$(get_data_path) + + # Remove from vmailbox + local vmailbox="$data_path/config/vmailbox" + sed -i "/^$email /d" "$vmailbox" 2>/dev/null + + # Remove from passfile + local passfile="$data_path/config/users" + sed -i "/^$email:/d" "$passfile" 2>/dev/null + + # Postmap + lxc-attach -n "$container" -- postmap /etc/postfix/vmailbox 2>/dev/null + + echo "User deleted: $email (mailbox preserved)" +} + +# List mail users +user_list() { + local data_path=$(get_data_path) + local passfile="$data_path/config/users" + + if [ -f "$passfile" ]; then + echo "Mail Users:" + cut -d: -f1 "$passfile" | while read email; do + local domain=$(echo "$email" | cut -d@ -f2) + local user=$(echo "$email" | cut -d@ -f1) + local maildir="$data_path/mail/$domain/$user" + local size=$(du -sh "$maildir" 2>/dev/null | awk '{print $1}') + printf " %-40s %s\n" "$email" "${size:-0}" + done + else + echo "No users configured" + fi +} + +# Change user password +user_passwd() { + local email="$1" + local password="$2" + + [ -z "$email" ] && { echo "Usage: user_passwd [new_password]"; return 1; } + + local container=$(get_container) + local data_path=$(get_data_path) + local passfile="$data_path/config/users" + + grep -q "^$email:" "$passfile" || { echo "User not found: $email"; return 1; } + + if [ -z "$password" ]; then + echo "Enter new password for $email:" + read -s password + fi + + local hash=$(lxc-attach -n "$container" -- doveadm pw -s SHA512-CRYPT -p "$password" 2>/dev/null) + [ -z "$hash" ] && hash=$(openssl passwd -6 "$password") + + sed -i "s|^$email:.*|$email:$hash|" "$passfile" + + echo "Password changed for: $email" +} + +# Add alias +alias_add() { + local alias="$1" + local target="$2" + + [ -z "$alias" ] || [ -z "$target" ] && { echo "Usage: alias_add "; return 1; } + + local container=$(get_container) + local data_path=$(get_data_path) + local valias="$data_path/config/valias" + + touch "$valias" + grep -q "^$alias " "$valias" || echo "$alias $target" >> "$valias" + + lxc-attach -n "$container" -- postmap /etc/postfix/valias 2>/dev/null + + echo "Alias added: $alias → $target" +} + +# List aliases +alias_list() { + local data_path=$(get_data_path) + local valias="$data_path/config/valias" + + if [ -f "$valias" ]; then + echo "Aliases:" + cat "$valias" | while read alias target; do + printf " %-40s → %s\n" "$alias" "$target" + done + else + echo "No aliases configured" + fi +} diff --git a/package/secubox/secubox-app-mailserver/files/usr/sbin/mailctl b/package/secubox/secubox-app-mailserver/files/usr/sbin/mailctl new file mode 100644 index 00000000..fd9a5c97 --- /dev/null +++ b/package/secubox/secubox-app-mailserver/files/usr/sbin/mailctl @@ -0,0 +1,442 @@ +#!/bin/sh +# SecuBox Mail Server Controller + +VERSION="1.0.0" +CONFIG="mailserver" +LIB_DIR="/usr/lib/mailserver" + +# Load libraries +. "$LIB_DIR/container.sh" +. "$LIB_DIR/users.sh" +. "$LIB_DIR/mesh.sh" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +log() { echo -e "${GREEN}[MAIL]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1"; } + +uci_get() { uci -q get ${CONFIG}.$1; } + +# ============================================================================ +# Install / Setup +# ============================================================================ + +cmd_install() { + local container=$(uci_get main.container) + container="${container:-mailserver}" + local data_path=$(uci_get main.data_path) + data_path="${data_path:-/srv/mailserver}" + + log "Installing mail server..." + + # Create container + container_create "$container" "$data_path" + local rc=$? + + [ $rc -ne 0 ] && return $rc + + # Start container + log "Starting container..." + lxc-start -n "$container" + sleep 5 + + # Run setup + log "Installing mail packages..." + lxc-attach -n "$container" -- /root/setup.sh + + log "Installation complete!" + echo "" + echo "Next steps:" + echo " 1. Configure domain: uci set mailserver.main.domain='yourdomain.com'" + echo " 2. Set up DNS: dnsctl mail-setup" + echo " 3. Add SSL cert: mailctl ssl-setup" + echo " 4. Add users: mailctl user-add user@yourdomain.com" + echo " 5. Enable: uci set mailserver.main.enabled=1 && uci commit" +} + +cmd_uninstall() { + local container=$(uci_get main.container) + container="${container:-mailserver}" + + warn "This will remove the mail server container and all data!" + echo "Continue? (yes/no)" + read confirm + [ "$confirm" != "yes" ] && { echo "Aborted"; return 1; } + + log "Stopping container..." + lxc-stop -n "$container" 2>/dev/null + + log "Removing container..." + rm -rf "/srv/lxc/$container" + + log "Mail server removed. Data in $(uci_get main.data_path) preserved." +} + +# ============================================================================ +# Service Control +# ============================================================================ + +cmd_start() { + /etc/init.d/mailserver start +} + +cmd_stop() { + /etc/init.d/mailserver stop +} + +cmd_restart() { + /etc/init.d/mailserver stop + sleep 2 + /etc/init.d/mailserver start +} + +cmd_status() { + local container=$(uci_get main.container) + container="${container:-mailserver}" + local domain=$(uci_get main.domain) + local hostname=$(uci_get main.hostname) + + echo "" + echo "========================================" + echo " SecuBox Mail Server v$VERSION" + echo "========================================" + echo "" + + # Container status + local state="stopped" + lxc-info -n "$container" 2>/dev/null | grep -q "RUNNING" && state="running" + echo " Container: $container ($state)" + echo " Domain: ${domain:-not set}" + echo " Hostname: ${hostname:-mail}.${domain:-example.com}" + echo "" + + # Port status + echo " Ports:" + local ports="25 587 465 993 995" + for port in $ports; do + if netstat -tln 2>/dev/null | grep -q ":$port "; then + echo -e " $port: ${GREEN}listening${NC}" + else + echo -e " $port: ${RED}closed${NC}" + fi + done + echo "" + + # User count + local data_path=$(uci_get main.data_path) + data_path="${data_path:-/srv/mailserver}" + local user_count=$(wc -l < "$data_path/config/users" 2>/dev/null || echo "0") + echo " Users: $user_count" + + # Storage + local storage=$(du -sh "$data_path" 2>/dev/null | awk '{print $1}') + echo " Storage: ${storage:-0}" + echo "" +} + +# ============================================================================ +# User Commands +# ============================================================================ + +cmd_user() { + local action="$1" + shift + + case "$action" in + add) user_add "$@" ;; + del) user_del "$@" ;; + list) user_list ;; + passwd) user_passwd "$@" ;; + *) + echo "User commands:" + echo " user add Add mail user" + echo " user del Delete mail user" + echo " user list List all users" + echo " user passwd Change password" + ;; + esac +} + +cmd_alias() { + local action="$1" + shift + + case "$action" in + add) alias_add "$@" ;; + list) alias_list ;; + *) + echo "Alias commands:" + echo " alias add Add email alias" + echo " alias list List aliases" + ;; + esac +} + +# ============================================================================ +# SSL Certificate +# ============================================================================ + +cmd_ssl_setup() { + local container=$(uci_get main.container) + container="${container:-mailserver}" + local domain=$(uci_get main.domain) + local hostname=$(uci_get main.hostname) + hostname="${hostname:-mail}" + local fqdn="${hostname}.${domain}" + + log "Setting up SSL for $fqdn..." + + # Check if acme.sh available + if ! command -v acme.sh >/dev/null 2>&1; then + error "acme.sh not installed. Install with: opkg install acme acme-dnsapi" + return 1 + fi + + # Use DNS-01 via dnsctl + log "Requesting certificate via DNS-01..." + dnsctl acme-dns01 "$fqdn" + + local cert_dir="/root/.acme.sh/${fqdn}" + local data_path=$(uci_get main.data_path) + data_path="${data_path:-/srv/mailserver}" + + if [ -f "$cert_dir/fullchain.cer" ]; then + cp "$cert_dir/fullchain.cer" "$data_path/ssl/fullchain.pem" + cp "$cert_dir/${fqdn}.key" "$data_path/ssl/privkey.pem" + chmod 600 "$data_path/ssl/"*.pem + + log "SSL certificates installed to $data_path/ssl/" + log "Restarting mail services..." + cmd_restart + else + error "Certificate generation failed" + return 1 + fi +} + +cmd_ssl_status() { + local data_path=$(uci_get main.data_path) + data_path="${data_path:-/srv/mailserver}" + + if [ -f "$data_path/ssl/fullchain.pem" ]; then + echo "SSL Certificate:" + openssl x509 -in "$data_path/ssl/fullchain.pem" -noout -subject -dates 2>/dev/null | sed 's/^/ /' + else + echo "No SSL certificate installed" + fi +} + +# ============================================================================ +# DNS Integration +# ============================================================================ + +cmd_dns_setup() { + local hostname=$(uci_get main.hostname) + hostname="${hostname:-mail}" + + log "Setting up DNS records..." + dnsctl mail-setup "$hostname" +} + +# ============================================================================ +# Mesh Commands +# ============================================================================ + +cmd_mesh() { + local action="$1" + shift + + case "$action" in + backup) mesh_backup ;; + restore) mesh_restore "$@" ;; + sync) mesh_sync "$@" ;; + add-peer) mesh_add_peer "$@" ;; + peers) mesh_list_peers ;; + enable) + uci set $CONFIG.mesh.enabled=1 + uci commit $CONFIG + log "Mesh sync enabled" + ;; + disable) + uci set $CONFIG.mesh.enabled=0 + uci commit $CONFIG + log "Mesh sync disabled" + ;; + *) + echo "Mesh commands:" + echo " mesh backup Create backup for mesh" + echo " mesh restore Restore from backup" + echo " mesh sync [push|pull] Sync with peers" + echo " mesh add-peer Add mesh peer" + echo " mesh peers List mesh peers" + echo " mesh enable Enable mesh sync" + echo " mesh disable Disable mesh sync" + ;; + esac +} + +# ============================================================================ +# Logs & Diagnostics +# ============================================================================ + +cmd_logs() { + local lines="${1:-50}" + local container=$(uci_get main.container) + container="${container:-mailserver}" + + lxc-attach -n "$container" -- tail -n "$lines" /var/log/mail.log 2>/dev/null +} + +cmd_test() { + local email="$1" + [ -z "$email" ] && { echo "Usage: mailctl test "; return 1; } + + local container=$(uci_get main.container) + container="${container:-mailserver}" + local domain=$(uci_get main.domain) + local hostname=$(uci_get main.hostname) + + log "Sending test email to $email..." + + lxc-attach -n "$container" -- sh -c "echo 'Test from SecuBox Mail Server' | mail -s 'Test Email' '$email'" + + log "Test email sent. Check inbox." +} + +# ============================================================================ +# Webmail Integration +# ============================================================================ + +cmd_webmail() { + local action="${1:-status}" + + case "$action" in + status) + local webmail=$(uci_get webmail.container) + webmail="${webmail:-secubox-webmail}" + if docker ps 2>/dev/null | grep -q "$webmail"; then + local port=$(uci_get webmail.port) + echo "Webmail: Running on port ${port:-8026}" + else + echo "Webmail: Not running" + fi + ;; + configure) + local domain=$(uci_get main.domain) + local hostname=$(uci_get main.hostname) + hostname="${hostname:-mail}" + local fqdn="${hostname}.${domain}" + + log "Configuring webmail to use $fqdn..." + + local webmail=$(uci_get webmail.container) + webmail="${webmail:-secubox-webmail}" + + # Update Roundcube config + docker exec "$webmail" sh -c "cat > /var/www/html/config/config.docker.inc.php << EOF + [options] + +Setup: + install Create and configure mail server + uninstall Remove mail server + dns-setup Set up MX/SPF/DKIM/DMARC via dnsctl + ssl-setup Obtain SSL certificate + +Service: + start Start mail server + stop Stop mail server + restart Restart mail server + status Show server status + +Users: + user add Add mail user + user del Delete mail user + user list List all users + user passwd Change password + +Aliases: + alias add Add email alias + alias list List aliases + +Mesh Backup: + mesh backup Create backup + mesh restore Restore from backup + mesh sync Sync with peers + mesh peers List mesh peers + +Webmail: + webmail status Show webmail status + webmail configure Configure for this server + +Diagnostics: + logs [lines] View mail logs + test Send test email + ssl-status Show SSL cert info + +Examples: + mailctl install + mailctl user add user@example.com + mailctl dns-setup + mailctl mesh sync push + +EOF +} + +# ============================================================================ +# Main +# ============================================================================ + +case "${1:-}" in + install) shift; cmd_install "$@" ;; + uninstall) shift; cmd_uninstall "$@" ;; + start) shift; cmd_start "$@" ;; + stop) shift; cmd_stop "$@" ;; + restart) shift; cmd_restart "$@" ;; + status) shift; cmd_status "$@" ;; + user) shift; cmd_user "$@" ;; + alias) shift; cmd_alias "$@" ;; + ssl-setup) shift; cmd_ssl_setup "$@" ;; + ssl-status) shift; cmd_ssl_status "$@" ;; + dns-setup) shift; cmd_dns_setup "$@" ;; + mesh) shift; cmd_mesh "$@" ;; + webmail) shift; cmd_webmail "$@" ;; + logs) shift; cmd_logs "$@" ;; + test) shift; cmd_test "$@" ;; + help|--help|-h|'') show_help ;; + *) error "Unknown command: $1"; show_help >&2; exit 1 ;; +esac + +exit 0