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:
CyberMind-FR 2026-02-05 10:40:32 +01:00
parent e13b6e4c8c
commit c6fb79ed3b
25 changed files with 3091 additions and 2 deletions

View File

@ -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.

View File

@ -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

View 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)))

View 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

View File

@ -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
});

View File

@ -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

View File

@ -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 }
}
}
}

View File

@ -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"]
}
}
}

View 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))

View 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)

View 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'

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View 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

View File

@ -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
```

View File

@ -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 ;;

View 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)))

View 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)

View File

@ -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'

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View 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