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 <noreply@anthropic.com>
This commit is contained in:
parent
e13b6e4c8c
commit
c6fb79ed3b
@ -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 <service> [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] <pubkey>` — add DKIM TXT record
|
||||
- Renamed `secbx-webmail` Docker container to `secubox-webmail` for consistency.
|
||||
|
||||
@ -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
|
||||
|
||||
29
package/secubox/luci-app-backup/Makefile
Normal file
29
package/secubox/luci-app-backup/Makefile
Normal file
@ -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)))
|
||||
39
package/secubox/luci-app-backup/README.md
Normal file
39
package/secubox/luci-app-backup/README.md
Normal file
@ -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
|
||||
@ -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
|
||||
});
|
||||
@ -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
|
||||
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
41
package/secubox/secubox-app-backup/Makefile
Normal file
41
package/secubox/secubox-app-backup/Makefile
Normal file
@ -0,0 +1,41 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=secubox-app-backup
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_MAINTAINER:=SecuBox <support@secubox.io>
|
||||
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))
|
||||
130
package/secubox/secubox-app-backup/README.md
Normal file
130
package/secubox/secubox-app-backup/README.md
Normal file
@ -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/<name>/` |
|
||||
| **Services** | `/srv/haproxy/`, `/srv/mitmproxy/`, `/srv/localai/`, `/srv/gitea/` |
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `lxc` - For container backup/restore
|
||||
- `tar` - Archive creation
|
||||
- `wget` - Gitea API communication (optional)
|
||||
25
package/secubox/secubox-app-backup/files/etc/config/backup
Normal file
25
package/secubox/secubox-app-backup/files/etc/config/backup
Normal file
@ -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'
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
437
package/secubox/secubox-app-backup/files/usr/sbin/secubox-backup
Normal file
437
package/secubox/secubox-app-backup/files/usr/sbin/secubox-backup
Normal file
@ -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 <backup_file> [--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 <name>"; 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 <name> <backup_file>"; return 1; }
|
||||
log "Restoring container: $name"
|
||||
container_restore "$name" "$backup"
|
||||
;;
|
||||
|
||||
backups)
|
||||
local name="$1"
|
||||
[ -z "$name" ] && { echo "Usage: secubox-backup container backups <name>"; return 1; }
|
||||
local storage=$(get_storage_path)
|
||||
container_list_backups "$name" "$storage/containers"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Container commands:"
|
||||
echo " list List containers"
|
||||
echo " backup <name> Backup container"
|
||||
echo " restore <n> <f> Restore container from backup"
|
||||
echo " backups <name> 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 <name> Create profile from current config"
|
||||
echo " apply <name> Apply profile"
|
||||
echo " share <name> 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 <command> [options]
|
||||
|
||||
Commands:
|
||||
create [--full|--config|--containers|--services]
|
||||
Create backup
|
||||
list [--local|--remote|--all]
|
||||
List backups
|
||||
restore <file> [--dry-run]
|
||||
Restore from backup
|
||||
status Show backup status
|
||||
cleanup Remove old backups
|
||||
|
||||
container list List LXC containers
|
||||
container backup <n> Backup container
|
||||
container restore <n> <f>
|
||||
Restore container
|
||||
|
||||
profile list List profiles
|
||||
profile create <n> Create profile
|
||||
profile apply <n> 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
|
||||
@ -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 '<public-key>' # 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
|
||||
```
|
||||
|
||||
|
||||
@ -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 <service> [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 <name>'"
|
||||
;;
|
||||
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 '<dkim-public-key>'"
|
||||
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] <public_key>"
|
||||
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 <service> [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] <pubkey> Add DKIM TXT record
|
||||
|
||||
SSL:
|
||||
acme-dns01 <domain> 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 ;;
|
||||
|
||||
39
package/secubox/secubox-app-mailserver/Makefile
Normal file
39
package/secubox/secubox-app-mailserver/Makefile
Normal file
@ -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)))
|
||||
208
package/secubox/secubox-app-mailserver/README.md
Normal file
208
package/secubox/secubox-app-mailserver/README.md
Normal file
@ -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 <file> # Restore from backup
|
||||
mailctl mesh sync push # Push to mesh peers
|
||||
mailctl mesh sync pull # Pull from mesh peers
|
||||
mailctl mesh add-peer <peer_id> # 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 | `<public-ip>` |
|
||||
| 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=<public-key>` |
|
||||
|
||||
## 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)
|
||||
@ -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'
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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 <peer_id>"; 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
|
||||
}
|
||||
@ -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 <email@domain>"; 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 <email@domain>"; 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 <email@domain> [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 <alias@domain> <target@domain>"; 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
|
||||
}
|
||||
442
package/secubox/secubox-app-mailserver/files/usr/sbin/mailctl
Normal file
442
package/secubox/secubox-app-mailserver/files/usr/sbin/mailctl
Normal file
@ -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 <email@domain> Add mail user"
|
||||
echo " user del <email@domain> Delete mail user"
|
||||
echo " user list List all users"
|
||||
echo " user passwd <email> Change password"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_alias() {
|
||||
local action="$1"
|
||||
shift
|
||||
|
||||
case "$action" in
|
||||
add) alias_add "$@" ;;
|
||||
list) alias_list ;;
|
||||
*)
|
||||
echo "Alias commands:"
|
||||
echo " alias add <alias> <target> 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 <file> Restore from backup"
|
||||
echo " mesh sync [push|pull] Sync with peers"
|
||||
echo " mesh add-peer <id> 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 <email@domain>"; 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
|
||||
<?php
|
||||
\\\$config['db_dsnw'] = 'sqlite:////var/roundcube/db/sqlite.db?mode=0646';
|
||||
\\\$config['imap_host'] = 'ssl://$fqdn:993';
|
||||
\\\$config['smtp_host'] = 'ssl://$fqdn:465';
|
||||
\\\$config['username_domain'] = '$domain';
|
||||
\\\$config['temp_dir'] = '/tmp/roundcube-temp';
|
||||
\\\$config['skin'] = 'elastic';
|
||||
\\\$config['plugins'] = array_filter(array_unique(array_merge(\\\$config['plugins'], ['archive', 'zipdownload'])));
|
||||
EOF"
|
||||
|
||||
log "Webmail configured. Restart with: docker restart $webmail"
|
||||
;;
|
||||
*)
|
||||
echo "Webmail commands:"
|
||||
echo " webmail status Show webmail status"
|
||||
echo " webmail configure Configure webmail for this server"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Help
|
||||
# ============================================================================
|
||||
|
||||
show_help() {
|
||||
cat << EOF
|
||||
SecuBox Mail Server v$VERSION
|
||||
|
||||
Usage: mailctl <command> [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 <email> Add mail user
|
||||
user del <email> Delete mail user
|
||||
user list List all users
|
||||
user passwd <email> Change password
|
||||
|
||||
Aliases:
|
||||
alias add <a> <t> Add email alias
|
||||
alias list List aliases
|
||||
|
||||
Mesh Backup:
|
||||
mesh backup Create backup
|
||||
mesh restore <file> 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 <email> 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
|
||||
Loading…
Reference in New Issue
Block a user