feat(p2p): Add MirrorBox auto-init, self-recovery, and ACL fixes

MirrorBox Auto-Init:
- Add blockchain-like gigogne P2P structure with peer zero (P0) genesis
- Auto-create self-mesh on page load with configurable depth
- Preserve MirrorBox peers during refresh cycles

Self-Recovery System:
- Add secubox-restore script for bootstrapping new OpenWrt boxes
- Generate customized bootstrap.sh in Gitea backups
- Support interactive and command-line restore modes

ACL Fixes:
- Add missing deploy/pull methods to luci-app-secubox-p2p ACL
- Add luci.gitea and luci.secubox-p2p access to luci-app-secubox ACL
- Fix null display issue in hub.js (changed to empty string)

Backup Enhancements:
- Fix syntax error in RPCD heredoc (openwrt_version line)
- Add branch reference to Gitea API calls (main branch)
- Include bootstrap.sh and secubox-restore in backup push

Documentation:
- Add comprehensive README.md for SecuBox P2P module

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-30 13:34:12 +01:00
parent 2687e863a6
commit a68d8b638f
7 changed files with 1320 additions and 70 deletions

View File

@ -2,9 +2,24 @@
'require view';
'require ui';
'require dom';
'require rpc';
'require secubox-p2p/api as P2PAPI';
'require poll';
// Gitea RPC for token generation
var callGiteaGenerateToken = rpc.declare({
object: 'luci.gitea',
method: 'generate_token',
params: ['username', 'token_name', 'scopes'],
expect: {}
});
var callGiteaGetStatus = rpc.declare({
object: 'luci.gitea',
method: 'get_status',
expect: {}
});
return view.extend({
// State
peers: [],
@ -76,12 +91,14 @@ return view.extend({
mode: 'gigogne', // gigogne = nested cycle, mono = single hop, full = all-to-all, ring = circular
cycleDepth: 3,
autoPropagate: true,
selfLoop: true
selfLoop: true,
autoSelfMesh: true // Auto-create peer zero MirrorBox on init
},
// Self-peer for testing
// Self-peer for MirrorBox mesh
selfPeer: null,
testMode: false,
mirrorBoxInit: false, // Track if MirrorBox is initialized
// Mesh Backup Config
meshBackupConfig: {
@ -191,6 +208,11 @@ return view.extend({
}).catch(function() {});
}
// Auto-init MirrorBox (peer zero) in load phase
if (self.distributionConfig.autoSelfMesh && !self.mirrorBoxInit) {
self.initMirrorBox();
}
return {};
});
},
@ -239,13 +261,23 @@ return view.extend({
refreshData: function() {
var self = this;
// Preserve MirrorBox peers before refresh
var mirrorBoxPeers = this.peers.filter(function(p) {
return p.isMirrorBox || p.isSelf || p.isGigogne;
});
return Promise.all([
P2PAPI.getPeers(),
P2PAPI.getServices(),
P2PAPI.getSharedServices(),
P2PAPI.healthCheck()
]).then(function(results) {
self.peers = results[0].peers || [];
// Merge API peers with preserved MirrorBox peers
var apiPeers = results[0].peers || [];
self.peers = mirrorBoxPeers.concat(apiPeers.filter(function(p) {
return !mirrorBoxPeers.some(function(m) { return m.id === p.id; });
}));
self.services = results[1].services || [];
self.sharedServices = results[2].shared_services || [];
self.health = results[3] || {};
@ -407,7 +439,7 @@ return view.extend({
'change': function(e) { self.distributionConfig.cycleDepth = parseInt(e.target.value); }
})
]),
this.testMode ? E('span', { 'class': 'test-badge' }, '🧪 TEST') : null
this.testMode ? E('span', { 'class': 'test-badge' }, '🧪 TEST') : ''
])
])
]);
@ -1310,7 +1342,62 @@ return view.extend({
ui.addNotification(null, E('p', 'WireGuard Mirror mode: ' + mode), 'info');
},
// ==================== Self Peer & Distribution ====================
// ==================== MirrorBox Auto-Init & Self Peer ====================
initMirrorBox: function() {
var self = this;
// Already initialized or no peers needed
if (this.mirrorBoxInit || !this.distributionConfig.autoSelfMesh) return;
this.mirrorBoxInit = true;
// Get hostname for peer zero naming
var hostname = (this.settings && this.settings.node_name) || 'secubox';
// Create peer zero - the genesis MirrorBox node
this.selfPeer = {
id: 'mirrorbox-0-' + Date.now(),
name: '👑 ' + hostname + ' (P0)',
address: '127.0.0.1',
status: 'online',
isSelf: true,
isMirrorBox: true,
peerNumber: 0,
services: this.services.slice(),
wgMirror: true,
blockchainGenesis: true
};
this.peers.unshift(this.selfPeer);
this.testMode = true;
// Create gigogne nested peers (blockchain-like structure)
var depth = this.distributionConfig.cycleDepth;
var prevId = this.selfPeer.id;
for (var i = 1; i < depth; i++) {
var nestedPeer = {
id: 'gigogne-' + i + '-' + Date.now(),
name: '🪆 Gigogne L' + i,
address: '127.0.0.' + (i + 1),
status: 'online',
isSelf: true,
isGigogne: true,
isMirrorBox: true,
level: i,
peerNumber: i,
parentId: prevId,
services: this.services.slice(),
wgMirror: true,
blockchainPrev: prevId
};
prevId = nestedPeer.id;
this.peers.push(nestedPeer);
}
console.log('🔗 MirrorBox initialized: peer zero + ' + (depth - 1) + ' gigogne levels');
},
addSelfPeer: function() {
var self = this;
this.testMode = true;
@ -1702,49 +1789,68 @@ return view.extend({
}
},
executeAutoCreate: function(serverUrl, repoName, repoDesc, token) {
executeAutoCreate: function(serverUrl, repoName, repoDesc, providedToken) {
var self = this;
ui.addNotification(null, E('p', '🚀 Auto-creating mesh repository...'), 'info');
// Step 1: Save Gitea config
P2PAPI.setGiteaConfig({
server_url: serverUrl,
repo_name: repoName,
access_token: token,
enabled: 1,
auto_backup: 1,
backup_on_change: 1
}).then(function() {
// Step 2: Create repository
ui.addNotification(null, E('p', '📦 Creating repository: ' + repoName), 'info');
return P2PAPI.createGiteaRepo(repoName, repoDesc, true, true);
}).then(function(result) {
if (result.success) {
// Update local state
self.giteaConfig.serverUrl = serverUrl;
self.giteaConfig.repoName = result.repo_name || repoName;
self.giteaConfig.repoOwner = result.owner || '';
self.giteaConfig.enabled = true;
self.giteaConfig.hasToken = true;
self.giteaConfig.lastFetch = Date.now();
// Step 1: Generate new token with full scopes via Gitea RPCD
ui.addNotification(null, E('p', '🔑 Generating access token...'), 'info');
ui.addNotification(null, E('p', '✅ Repository created: ' + self.giteaConfig.repoOwner + '/' + self.giteaConfig.repoName), 'success');
callGiteaGenerateToken('gandalf', repoName + '-token', 'write:repository,write:user,read:user')
.then(function(result) {
var token = providedToken;
if (result && result.result && result.result.token) {
token = result.result.token;
ui.addNotification(null, E('p', '✅ Token generated successfully'), 'success');
} else if (!token) {
throw new Error('Failed to generate token and no token provided');
}
// Step 3: Push initial state
ui.addNotification(null, E('p', '📤 Pushing initial mesh state...'), 'info');
return P2PAPI.pushGiteaBackup('Initial SecuBox mesh configuration', {});
} else {
throw new Error(result.error || 'Failed to create repository');
}
}).then(function(pushResult) {
if (pushResult && pushResult.success) {
ui.addNotification(null, E('p', '🎉 Mesh repository ready! ' + pushResult.files_pushed + ' files uploaded'), 'success');
self.refreshGiteaCommits();
}
}).catch(function(err) {
ui.addNotification(null, E('p', '❌ Auto-setup failed: ' + err.message), 'error');
});
// Step 2: Save Gitea config with new token
ui.addNotification(null, E('p', '💾 Saving configuration...'), 'info');
return P2PAPI.setGiteaConfig({
server_url: serverUrl,
repo_name: repoName,
access_token: token,
enabled: 1,
auto_backup: 1,
backup_on_change: 1
});
})
.then(function() {
// Step 3: Create repository
ui.addNotification(null, E('p', '📦 Creating repository: ' + repoName), 'info');
return P2PAPI.createGiteaRepo(repoName, repoDesc, true, true);
})
.then(function(result) {
if (result.success) {
// Update local state
self.giteaConfig.serverUrl = serverUrl;
self.giteaConfig.repoName = result.repo_name || repoName;
self.giteaConfig.repoOwner = result.owner || '';
self.giteaConfig.enabled = true;
self.giteaConfig.hasToken = true;
self.giteaConfig.lastFetch = Date.now();
ui.addNotification(null, E('p', '✅ Repository created: ' + self.giteaConfig.repoOwner + '/' + self.giteaConfig.repoName), 'success');
// Step 4: Push initial state
ui.addNotification(null, E('p', '📤 Pushing initial mesh state...'), 'info');
return P2PAPI.pushGiteaBackup('Initial SecuBox mesh configuration', {});
} else {
throw new Error(result.error || 'Failed to create repository');
}
})
.then(function(pushResult) {
if (pushResult && pushResult.success) {
ui.addNotification(null, E('p', '🎉 Mesh repository ready! ' + pushResult.files_pushed + ' files uploaded'), 'success');
self.refreshGiteaCommits();
}
})
.catch(function(err) {
ui.addNotification(null, E('p', '❌ Auto-setup failed: ' + err.message), 'error');
});
},
fetchGiteaCommits: function() {
@ -2340,7 +2446,7 @@ return view.extend({
E('div', { 'class': 'panel-title' }, [
E('span', {}, '👥'),
E('span', {}, 'Connected Peers'),
this.testMode ? E('span', { 'class': 'badge test' }, '🧪 TEST') : null
this.testMode ? E('span', { 'class': 'badge test' }, '🧪 TEST') : ''
]),
E('button', { 'class': 'btn small', 'click': function() { self.discoverPeers(); } }, '🔍 Discover')
]),
@ -2377,7 +2483,7 @@ return view.extend({
E('div', { 'class': 'panel-actions' }, [
E('button', { 'class': 'btn', 'click': function() { self.showAddPeerModal(); } }, ' Add Peer'),
this.testMode ?
E('button', { 'class': 'btn', 'click': function() { self.removeSelfPeer(); } }, '🗑️ Clear Test') : null
E('button', { 'class': 'btn', 'click': function() { self.removeSelfPeer(); } }, '🗑️ Clear Test') : ''
])
]);
},

View File

@ -17,7 +17,9 @@
"get_gitea_config",
"list_gitea_repos",
"get_gitea_commits",
"list_local_backups"
"list_local_backups",
"get_dns_bridge_config",
"get_wg_mirror_config"
],
"uci": ["get", "state"]
},
@ -40,7 +42,17 @@
"push_gitea_backup",
"pull_gitea_backup",
"create_local_backup",
"restore_local_backup"
"restore_local_backup",
"deploy_registry",
"deploy_registry_entry",
"deploy_services",
"deploy_local_services",
"deploy_service",
"pull_mesh_services",
"pull_service",
"set_dns_bridge_config",
"set_wg_mirror_config",
"sync_wg_mirror"
],
"uci": ["set", "delete", "commit", "apply"]
},

View File

@ -1,11 +1,53 @@
'use strict';
'require view';
'require ui';
'require rpc';
'require secubox/api as API';
'require secubox-theme/theme as Theme';
'require secubox/nav as SecuNav';
'require secubox-portal/header as SbHeader';
// P2P Mesh RPC declarations
var callGetGiteaConfig = rpc.declare({
object: 'luci.secubox-p2p',
method: 'get_gitea_config',
expect: {}
});
var callSetGiteaConfig = rpc.declare({
object: 'luci.secubox-p2p',
method: 'set_gitea_config',
params: ['config'],
expect: { success: false }
});
var callCreateGiteaRepo = rpc.declare({
object: 'luci.secubox-p2p',
method: 'create_gitea_repo',
params: ['name', 'description', 'private', 'init_readme'],
expect: { success: false }
});
var callPushGiteaBackup = rpc.declare({
object: 'luci.secubox-p2p',
method: 'push_gitea_backup',
params: ['message', 'components'],
expect: { success: false }
});
var callGiteaGenerateToken = rpc.declare({
object: 'luci.gitea',
method: 'generate_token',
params: ['username', 'token_name', 'scopes'],
expect: {}
});
var callGiteaGetStatus = rpc.declare({
object: 'luci.gitea',
method: 'get_status',
expect: {}
});
// Load theme resources
document.head.appendChild(E('link', {
'rel': 'stylesheet',
@ -37,7 +79,9 @@ return view.extend({
return Promise.all([
API.getFirstRunStatus(),
API.listApps(),
API.listProfiles()
API.listProfiles(),
callGetGiteaConfig().catch(function() { return {}; }),
callGiteaGetStatus().catch(function() { return {}; })
]);
},
@ -51,8 +95,12 @@ return view.extend({
// Filter to only show apps with wizards
this.appList = allApps.filter(function(app) { return app.has_wizard === true; });
this.profileList = Array.isArray(payload[2]) ? payload[2] : (payload[2] && payload[2].profiles) || [];
this.p2pConfig = payload[3] || {};
this.giteaStatus = payload[4] || {};
console.log('[SecuBox Wizard] Filtered appList (has_wizard only):', this.appList);
console.log('[SecuBox Wizard] Parsed profileList:', this.profileList);
console.log('[SecuBox Wizard] P2P Config:', this.p2pConfig);
console.log('[SecuBox Wizard] Gitea Status:', this.giteaStatus);
var container = E('div', { 'class': 'secubox-wizard-page' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/core/variables.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox/common.css') }),
@ -82,12 +130,16 @@ return view.extend({
},
renderFirstRunCard: function() {
var self = this;
var data = this.firstRun || {};
var p2pData = this.p2pConfig || {};
var giteaData = this.giteaStatus || {};
var steps = [
{ icon: '🔐', label: _('Secure Admin Account'), description: _('Set a LuCI/root password to protect the router.'), complete: !!data.password_set, content: this.renderPasswordStep(data) },
{ icon: '🌍', label: _('Timezone & Locale'), description: _('Align system time with your region.'), complete: false, content: this.renderTimezoneStep(data) },
{ icon: '💾', label: _('Storage Path'), description: _('Choose where SecuBox apps store data (USB/NAS recommended).'), complete: !!data.storage_ready, content: this.renderStorageStep(data) },
{ icon: '🛡️', label: _('Network Mode'), description: _('Pick a default SecuBox network mode (router or DMZ).'), complete: false, content: this.renderModeStep(data) }
{ icon: '🛡️', label: _('Network Mode'), description: _('Pick a default SecuBox network mode (router or DMZ).'), complete: false, content: this.renderModeStep(data) },
{ icon: '🌐', label: _('P2P Mesh Backup'), description: _('Auto-configure Gitea repository for mesh config versioning.'), complete: !!(p2pData.enabled && p2pData.repo_name), content: this.renderP2PMeshStep(p2pData, giteaData) }
];
return E('div', { 'class': 'sb-wizard-card' }, [
@ -159,6 +211,112 @@ return view.extend({
]);
},
renderP2PMeshStep: function(p2pData, giteaData) {
var self = this;
var giteaRunning = giteaData && giteaData.result && giteaData.result.running;
var repoConfigured = p2pData && p2pData.enabled && p2pData.repo_name;
if (repoConfigured) {
return E('div', { 'class': 'sb-wizard-inline' }, [
E('div', { 'class': 'sb-wizard-status ok' }, [
_('Repository: '),
E('strong', {}, (p2pData.repo_owner || 'user') + '/' + p2pData.repo_name)
]),
E('a', {
'class': 'cbi-button cbi-button-action',
'href': L.url('admin', 'secubox', 'p2p-hub')
}, _('Open P2P Hub'))
]);
}
if (!giteaRunning) {
return E('div', { 'class': 'sb-wizard-inline' }, [
E('div', { 'class': 'sb-wizard-status warn' }, _('Gitea not running')),
E('a', {
'class': 'cbi-button cbi-button-action',
'href': L.url('admin', 'services', 'gitea')
}, _('Start Gitea'))
]);
}
// Gitea running but repo not configured - show auto-setup
return E('div', { 'class': 'sb-wizard-inline' }, [
E('div', { 'class': 'sb-wizard-status warn' }, _('Not configured')),
E('button', {
'class': 'cbi-button cbi-button-action',
'id': 'btn-p2p-auto-setup',
'click': function() { self.autoSetupP2PMesh(); }
}, _('🚀 Auto Setup'))
]);
},
autoSetupP2PMesh: function() {
var self = this;
var btn = document.getElementById('btn-p2p-auto-setup');
if (btn) btn.disabled = true;
ui.showModal(_('P2P Mesh Auto Setup'), [
E('div', { 'class': 'spinning', 'style': 'margin: 20px auto;' }),
E('p', { 'id': 'p2p-setup-status', 'style': 'text-align: center;' }, _('Initializing...'))
]);
var updateStatus = function(msg) {
var el = document.getElementById('p2p-setup-status');
if (el) el.textContent = msg;
};
var hostname = (this.firstRun && this.firstRun.hostname) || 'secubox';
var repoName = hostname.toLowerCase().replace(/[^a-z0-9-]/g, '-') + '-p2p';
// Step 1: Generate token with proper scopes
updateStatus(_('Generating access token...'));
callGiteaGenerateToken('gandalf', repoName + '-token', 'write:repository,write:user,read:user')
.then(function(result) {
if (!result || !result.result || !result.result.token) {
throw new Error(_('Failed to generate token'));
}
var token = result.result.token;
// Step 2: Save token to P2P config
updateStatus(_('Saving configuration...'));
return callSetGiteaConfig({
server_url: 'http://localhost:3000',
repo_name: repoName,
access_token: token,
enabled: 1,
auto_backup: 1
}).then(function() { return token; });
})
.then(function(token) {
// Step 3: Create repository
updateStatus(_('Creating repository: ') + repoName);
return callCreateGiteaRepo(repoName, 'SecuBox P2P Mesh configuration', true, true);
})
.then(function(result) {
if (!result || !result.success) {
throw new Error(result && result.error ? result.error : _('Failed to create repository'));
}
// Step 4: Push initial config
updateStatus(_('Pushing initial configuration...'));
return callPushGiteaBackup('Initial mesh configuration from wizard', {});
})
.then(function(result) {
ui.hideModal();
if (result && result.success) {
ui.addNotification(null, E('p', {}, _('🎉 P2P Mesh setup complete! Repository created and initial config pushed.')), 'info');
setTimeout(function() { window.location.reload(); }, 1500);
} else {
throw new Error(_('Failed to push configuration'));
}
})
.catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', {}, '❌ ' + (err.message || err)), 'error');
if (btn) btn.disabled = false;
});
},
renderAppsCard: function() {
var apps = this.appList || [];
return E('div', { 'class': 'sb-wizard-card' }, [

View File

@ -45,6 +45,15 @@
"p2p_get_peer_catalog",
"p2p_get_shared_services"
],
"luci.gitea": [
"get_status"
],
"luci.secubox-p2p": [
"get_gitea_config",
"list_gitea_repos",
"get_gitea_commits",
"list_local_backups"
],
"uci": [
"get",
"state"
@ -90,6 +99,18 @@
"p2p_sync_catalog",
"p2p_broadcast_command"
],
"luci.gitea": [
"generate_token",
"create_repo"
],
"luci.secubox-p2p": [
"set_gitea_config",
"create_gitea_repo",
"push_gitea_backup",
"pull_gitea_backup",
"create_local_backup",
"restore_local_backup"
],
"uci": [
"set",
"delete",

View File

@ -0,0 +1,342 @@
# SecuBox P2P Mesh Network
Distributed peer-to-peer mesh networking for SecuBox appliances with integrated backup, recovery, and federation capabilities.
## Overview
SecuBox P2P enables multiple SecuBox appliances to form a distributed mesh network for:
- **Service Discovery**: Automatically discover and connect to peer SecuBox nodes
- **Configuration Sync**: Share and synchronize configurations across the mesh
- **Distributed Backup**: Version-controlled backups via Gitea integration
- **Self-Recovery**: Bootstrap new appliances from existing backups
- **MaaS Federation**: Mesh-as-a-Service for distributed security infrastructure
## Architecture
```
┌─────────────────┐
│ Gitea Server │
│ (Version Ctrl) │
└────────┬────────┘
┌────────────────────┼────────────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ SecuBox │◄────────►│ SecuBox │◄────────►│ SecuBox │
│ Node A │ │ Node B │ │ Node C │
│ (Leader)│ │ (Peer) │ │ (Peer) │
└─────────┘ └─────────┘ └─────────┘
│ │ │
WireGuard WireGuard WireGuard
Tunnel Tunnel Tunnel
```
## Features
### Mesh Networking
| Feature | Description |
|---------|-------------|
| **Peer Discovery** | mDNS/DNS-SD based automatic peer discovery |
| **WireGuard VPN** | Encrypted mesh tunnels between nodes |
| **HAProxy LB** | Load balancing across mesh services |
| **DNS Integration** | Mesh-aware DNS resolution |
### Backup & Recovery
| Feature | Description |
|---------|-------------|
| **Gitea Integration** | Git-based versioned backups |
| **15 Component Types** | Comprehensive appliance backup |
| **Bootstrap Script** | One-command recovery for new boxes |
| **Historical Tracking** | Full audit trail of changes |
### Topology Modes
- **Full Mesh**: Every node connects to every other node
- **Star**: Central hub with spoke connections
- **Ring**: Circular topology with neighbor connections
- **Tree**: Hierarchical parent-child structure
## Installation
```bash
opkg update
opkg install secubox-p2p luci-app-secubox-p2p
```
## Configuration
### UCI Configuration
```bash
# /etc/config/secubox-p2p
config p2p 'settings'
option enabled '1'
option node_name 'secubox-node'
option discovery_enabled '1'
option sync_interval '300'
config gitea 'gitea'
option enabled '1'
option server_url 'http://localhost:3000'
option repo_owner 'admin'
option repo_name 'secubox-backup'
option access_token 'your-token-here'
option auto_backup '1'
option backup_interval '3600'
```
### Manual Configuration
```bash
# Enable P2P mesh
uci set secubox-p2p.settings.enabled='1'
uci set secubox-p2p.settings.node_name='my-secubox'
uci commit secubox-p2p
# Configure Gitea backup
uci set secubox-p2p.gitea.enabled='1'
uci set secubox-p2p.gitea.server_url='http://gitea.local:3000'
uci set secubox-p2p.gitea.repo_owner='admin'
uci set secubox-p2p.gitea.repo_name='secubox-backup'
uci set secubox-p2p.gitea.access_token='your-token'
uci commit secubox-p2p
# Restart service
/etc/init.d/secubox-p2p restart
```
## Usage
### Command Line
```bash
# Peer management
secubox-p2p peers # List connected peers
secubox-p2p discover # Discover new peers
secubox-p2p add-peer <addr> # Add peer manually
# Service management
secubox-p2p services # List local services
secubox-p2p shared-services # List mesh-shared services
# Sync operations
secubox-p2p sync # Sync with all peers
```
### RPCD API
All functions are available via ubus:
```bash
# Peer operations
ubus call luci.secubox-p2p get_peers
ubus call luci.secubox-p2p discover '{"timeout":5}'
ubus call luci.secubox-p2p add_peer '{"address":"10.0.0.2","name":"peer1"}'
# Gitea backup
ubus call luci.secubox-p2p push_gitea_backup '{"message":"Daily backup"}'
ubus call luci.secubox-p2p pull_gitea_backup '{"commit_sha":"abc123"}'
ubus call luci.secubox-p2p list_gitea_repos
ubus call luci.secubox-p2p get_gitea_commits '{"limit":10}'
# Local backup
ubus call luci.secubox-p2p create_local_backup '{"name":"pre-upgrade"}'
ubus call luci.secubox-p2p list_local_backups
ubus call luci.secubox-p2p restore_local_backup '{"backup_id":"20260130-120000"}'
```
## Backup Components
The backup system captures 15 component categories:
| Component | Path | Description |
|-----------|------|-------------|
| `configs` | `/etc/config/` | UCI configuration files |
| `profiles` | `/usr/share/secubox/profiles/` | Deployment profiles |
| `presets` | `/etc/secubox/presets/` | Settings presets |
| `manifests` | `/etc/secubox/manifests/` | App manifests |
| `scripts` | `/usr/share/secubox/scripts/` | Custom scripts |
| `macros` | `/etc/secubox/macros/` | Automation macros |
| `workflows` | `/etc/secubox/workflows/` | CI/CD workflows |
| `packages` | - | Installed package list |
| `services` | - | Service states |
| `cron` | `/etc/crontabs/` | Scheduled tasks |
| `ssh` | `/etc/dropbear/` | SSH keys & config |
| `certificates` | `/etc/acme/`, `/etc/ssl/` | TLS certificates |
| `haproxy` | `/etc/haproxy/` | Load balancer config |
| `dns` | `/etc/dnsmasq.d/` | DNS configuration |
| `device` | - | Hardware/system info |
## Self-Recovery
### Quick Bootstrap
Deploy SecuBox to a new OpenWrt box with one command:
```bash
# From the Gitea repository
wget -qO- http://gitea.local:3000/user/repo/raw/branch/main/bootstrap.sh | sh
# Or using curl
curl -sL http://gitea.local:3000/user/repo/raw/branch/main/bootstrap.sh | sh
```
### Manual Recovery
```bash
# Interactive mode
secubox-restore -i
# Direct restore
secubox-restore http://gitea.local:3000 admin secubox-backup [token]
# Restore from specific branch
secubox-restore -b develop http://gitea.local:3000 admin secubox-backup
```
### Recovery Options
```
secubox-restore [options] <server-url> <repo-owner> <repo-name> [token]
Options:
-i, --interactive Interactive mode with prompts
-b, --branch <name> Git branch to restore from (default: main)
--include-network Also restore network/wireless/firewall configs
-h, --help Show help message
```
## LuCI Web Interface
Access the P2P Hub at: **SecuBox > P2P Mesh > Hub**
### Dashboard Features
- **Globe Visualization**: Interactive mesh topology view
- **Status Indicators**: System, DNS, WireGuard, Load Balancer status
- **Peer Counters**: Connected peers, online nodes, shared services
- **Quick Actions**: Discover, Sync All, Add Peer, Self Peer
### Gitea Integration Tab
- **Repository Setup**: Configure Gitea server and credentials
- **Auto-Backup**: Enable scheduled backups
- **Commit History**: View backup history with restore options
- **Token Generation**: Create access tokens with proper scopes
## Security
### Authentication
- Gitea tokens require specific scopes:
- `write:repository` - Push backups
- `read:user` - Verify identity
- `write:user` - Create tokens (for auto-setup)
### Encryption
- All mesh traffic encrypted via WireGuard
- Gitea communication over HTTPS (recommended)
- SSH keys backed up securely
### Access Control
- RPCD ACL controls API access
- Per-user Gitea permissions
- Network-level firewall rules
## Troubleshooting
### Common Issues
**Peer discovery not working:**
```bash
# Check mDNS/avahi
/etc/init.d/avahi-daemon status
# Verify firewall allows mDNS (port 5353/udp)
uci show firewall | grep mdns
```
**Gitea backup fails:**
```bash
# Test API connectivity
curl -s http://gitea:3000/api/v1/user \
-H "Authorization: token YOUR_TOKEN"
# Check token scopes
ubus call luci.secubox-p2p get_gitea_config
```
**WireGuard tunnel not establishing:**
```bash
# Check WireGuard status
wg show
# Verify peer keys
uci show wireguard
```
### Logs
```bash
# P2P service logs
logread | grep secubox-p2p
# RPCD logs
logread | grep rpcd
```
## API Reference
### Peer Management
| Method | Parameters | Description |
|--------|------------|-------------|
| `get_peers` | - | List all peers |
| `add_peer` | `address`, `name` | Add new peer |
| `remove_peer` | `peer_id` | Remove peer |
| `discover` | `timeout` | Discover peers |
### Gitea Operations
| Method | Parameters | Description |
|--------|------------|-------------|
| `get_gitea_config` | - | Get Gitea settings |
| `set_gitea_config` | `config` | Update settings |
| `create_gitea_repo` | `name`, `description`, `private` | Create repository |
| `list_gitea_repos` | - | List repositories |
| `get_gitea_commits` | `limit` | Get commit history |
| `push_gitea_backup` | `message`, `components` | Push backup |
| `pull_gitea_backup` | `commit_sha` | Restore from commit |
### Local Backup
| Method | Parameters | Description |
|--------|------------|-------------|
| `create_local_backup` | `name`, `components` | Create backup |
| `list_local_backups` | - | List backups |
| `restore_local_backup` | `backup_id` | Restore backup |
## Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Test on OpenWrt device
5. Submit a pull request
## License
GPL-2.0 - See LICENSE file for details.
## Related Projects
- [SecuBox Core](../secubox-core/) - Core SecuBox functionality
- [LuCI App SecuBox](../luci-app-secubox/) - Main dashboard
- [LuCI App SecuBox P2P](../luci-app-secubox-p2p/) - P2P web interface
- [SecuBox Gitea](../luci-app-gitea/) - Gitea container management

View File

@ -0,0 +1,446 @@
#!/bin/sh
#
# SecuBox Self-Recovery Bootstrap Script
# Downloads and restores a SecuBox appliance configuration from Gitea
#
# Usage:
# curl -sL http://gitea-server/user/repo/raw/branch/master/bootstrap.sh | sh
# # OR
# wget -qO- http://gitea-server/user/repo/raw/branch/master/bootstrap.sh | sh
# # OR
# secubox-restore <gitea-url> <repo-owner> <repo-name> [access-token]
#
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
print_banner() {
cat << 'EOF'
____ ____
/ ___| ___ ___ _ | __ ) _____ __
\___ \ / _ \/ __| | | | _ \ / _ \ \/ /
___) | __/ (__| |_| | |_) | (_) > <
|____/ \___|\___|\__,_|____/ \___/_/\_\
Self-Recovery Bootstrap v1.0
EOF
}
# Check if we're on OpenWrt
check_openwrt() {
if [ ! -f /etc/openwrt_release ]; then
log_error "This script must be run on OpenWrt"
exit 1
fi
. /etc/openwrt_release
log_info "Detected OpenWrt $DISTRIB_RELEASE on $(cat /tmp/sysinfo/model 2>/dev/null || echo 'unknown device')"
}
# Install dependencies if missing
install_deps() {
log_info "Checking dependencies..."
# Required packages
for pkg in curl git-http jsonfilter; do
if ! command -v "$pkg" >/dev/null 2>&1 && ! opkg list-installed | grep -q "^$pkg "; then
log_info "Installing $pkg..."
opkg update >/dev/null 2>&1 || true
opkg install "$pkg" 2>/dev/null || log_warn "Could not install $pkg"
fi
done
log_success "Dependencies ready"
}
# Fetch manifest from Gitea repository
fetch_manifest() {
local server_url="$1"
local repo_owner="$2"
local repo_name="$3"
local token="$4"
local branch="${5:-master}"
log_info "Fetching backup manifest..."
local manifest_url="${server_url}/api/v1/repos/${repo_owner}/${repo_name}/contents/manifest.json?ref=${branch}"
if [ -n "$token" ]; then
response=$(curl -sL "$manifest_url" -H "Authorization: token $token")
else
response=$(curl -sL "$manifest_url")
fi
# Check for error
if echo "$response" | jsonfilter -e '@.message' 2>/dev/null | grep -qi "not found"; then
log_error "Repository or manifest not found"
return 1
fi
# Decode base64 content
content=$(echo "$response" | jsonfilter -e '@.content' 2>/dev/null | base64 -d 2>/dev/null)
if [ -z "$content" ]; then
log_error "Failed to fetch manifest"
return 1
fi
echo "$content"
}
# Fetch and restore a file from Gitea
fetch_file() {
local server_url="$1"
local repo_owner="$2"
local repo_name="$3"
local file_path="$4"
local dest_path="$5"
local token="$6"
local branch="${7:-master}"
local file_url="${server_url}/api/v1/repos/${repo_owner}/${repo_name}/contents/${file_path}?ref=${branch}"
if [ -n "$token" ]; then
response=$(curl -sL "$file_url" -H "Authorization: token $token")
else
response=$(curl -sL "$file_url")
fi
# Check if it's a file (has content) or directory
content=$(echo "$response" | jsonfilter -e '@.content' 2>/dev/null)
if [ -n "$content" ]; then
# Single file - decode and save
mkdir -p "$(dirname "$dest_path")"
echo "$content" | base64 -d > "$dest_path" 2>/dev/null
return 0
fi
return 1
}
# Fetch directory contents recursively
fetch_directory() {
local server_url="$1"
local repo_owner="$2"
local repo_name="$3"
local dir_path="$4"
local dest_base="$5"
local token="$6"
local branch="${7:-master}"
local dir_url="${server_url}/api/v1/repos/${repo_owner}/${repo_name}/contents/${dir_path}?ref=${branch}"
if [ -n "$token" ]; then
response=$(curl -sL "$dir_url" -H "Authorization: token $token")
else
response=$(curl -sL "$dir_url")
fi
# Parse each entry
echo "$response" | jsonfilter -e '@[*].path' 2>/dev/null | while read -r path; do
type=$(echo "$response" | jsonfilter -e "@[@.path='$path'].type" 2>/dev/null)
if [ "$type" = "file" ]; then
local rel_path="${path#$dir_path/}"
local dest_path="${dest_base}/${rel_path}"
fetch_file "$server_url" "$repo_owner" "$repo_name" "$path" "$dest_path" "$token" "$branch"
elif [ "$type" = "dir" ]; then
fetch_directory "$server_url" "$repo_owner" "$repo_name" "$path" "$dest_base" "$token" "$branch"
fi
done
}
# Restore configs from backup
restore_configs() {
local backup_dir="$1"
local configs_dir="$backup_dir/configs"
if [ ! -d "$configs_dir" ]; then
log_warn "No configs directory found in backup"
return 0
fi
log_info "Restoring UCI configurations..."
local count=0
for cfg in "$configs_dir"/*; do
[ -f "$cfg" ] || continue
local name=$(basename "$cfg")
# Skip system-critical configs unless explicitly requested
case "$name" in
network|wireless|firewall)
log_warn "Skipping critical config: $name (use --include-network to restore)"
continue
;;
esac
cp "$cfg" "/etc/config/$name"
count=$((count + 1))
done
log_success "Restored $count configuration files"
}
# Restore scripts
restore_scripts() {
local backup_dir="$1"
local scripts_dir="$backup_dir/scripts"
if [ ! -d "$scripts_dir" ]; then
return 0
fi
log_info "Restoring custom scripts..."
mkdir -p /usr/share/secubox/scripts
cp -r "$scripts_dir"/* /usr/share/secubox/scripts/ 2>/dev/null
chmod +x /usr/share/secubox/scripts/* 2>/dev/null
log_success "Scripts restored"
}
# Restore cron jobs
restore_cron() {
local backup_dir="$1"
local cron_file="$backup_dir/cron/root"
if [ ! -f "$cron_file" ]; then
return 0
fi
log_info "Restoring cron jobs..."
# Append to existing crontab (don't overwrite)
cat "$cron_file" >> /etc/crontabs/root 2>/dev/null
/etc/init.d/cron restart 2>/dev/null
log_success "Cron jobs restored"
}
# Install SecuBox packages if not present
install_secubox() {
log_info "Checking SecuBox installation..."
# Check if secubox-core is installed
if ! opkg list-installed | grep -q "secubox-core"; then
log_info "SecuBox not installed, attempting installation..."
# Try to add SecuBox feed
if ! grep -q "secubox" /etc/opkg/customfeeds.conf 2>/dev/null; then
log_warn "SecuBox feed not configured"
log_info "Please configure SecuBox feed manually and re-run this script"
return 1
fi
opkg update
opkg install secubox-core luci-app-secubox
fi
log_success "SecuBox is installed"
}
# Main restore function
do_restore() {
local server_url="$1"
local repo_owner="$2"
local repo_name="$3"
local token="$4"
local branch="${5:-master}"
# Create temp directory
local backup_dir="/tmp/secubox-restore-$$"
mkdir -p "$backup_dir"
# Fetch manifest first
manifest=$(fetch_manifest "$server_url" "$repo_owner" "$repo_name" "$token" "$branch")
if [ -z "$manifest" ]; then
log_error "Failed to fetch backup manifest"
rm -rf "$backup_dir"
return 1
fi
# Display backup info
local backup_hostname=$(echo "$manifest" | jsonfilter -e '@.hostname' 2>/dev/null)
local backup_timestamp=$(echo "$manifest" | jsonfilter -e '@.timestamp' 2>/dev/null)
local backup_version=$(echo "$manifest" | jsonfilter -e '@.secubox_version' 2>/dev/null)
echo ""
log_info "Backup Information:"
echo " Source Hostname: $backup_hostname"
echo " Backup Date: $backup_timestamp"
echo " SecuBox Version: $backup_version"
echo ""
# Ask for confirmation if interactive
if [ -t 0 ]; then
printf "Proceed with restore? [y/N] "
read -r confirm
case "$confirm" in
[yY][eE][sS]|[yY]) ;;
*) log_info "Restore cancelled"; rm -rf "$backup_dir"; return 0 ;;
esac
fi
# Fetch each component
log_info "Downloading backup components..."
for component in configs profiles presets manifests scripts cron ssh certificates; do
log_info "Fetching $component..."
fetch_directory "$server_url" "$repo_owner" "$repo_name" "$component" "$backup_dir/$component" "$token" "$branch" 2>/dev/null || true
done
# Perform restore
log_info "Applying backup..."
restore_configs "$backup_dir"
restore_scripts "$backup_dir"
restore_cron "$backup_dir"
# Save Gitea config for future backups
uci set secubox-p2p.gitea=gitea
uci set secubox-p2p.gitea.enabled='1'
uci set secubox-p2p.gitea.server_url="$server_url"
uci set secubox-p2p.gitea.repo_owner="$repo_owner"
uci set secubox-p2p.gitea.repo_name="$repo_name"
[ -n "$token" ] && uci set secubox-p2p.gitea.access_token="$token"
uci commit secubox-p2p
# Cleanup
rm -rf "$backup_dir"
log_success "Restore completed!"
echo ""
log_info "Please reboot to apply all changes: reboot"
}
# Interactive mode - prompt for settings
interactive_mode() {
print_banner
echo ""
check_openwrt
install_deps
echo ""
log_info "Enter Gitea repository details:"
echo ""
printf "Gitea Server URL (e.g., http://192.168.1.1:3000): "
read -r server_url
printf "Repository Owner (username): "
read -r repo_owner
printf "Repository Name: "
read -r repo_name
printf "Access Token (leave empty for public repos): "
read -r token
echo ""
do_restore "$server_url" "$repo_owner" "$repo_name" "$token"
}
# Show usage
usage() {
cat << EOF
SecuBox Self-Recovery Bootstrap Script
Usage:
$0 [options] <server-url> <repo-owner> <repo-name> [access-token]
$0 --interactive
Options:
--interactive, -i Run in interactive mode with prompts
--branch, -b Git branch to restore from (default: master)
--include-network Also restore network/wireless/firewall configs
--help, -h Show this help message
Examples:
# Interactive mode
$0 -i
# Direct restore from public repo
$0 http://gitea.local:3000 admin secubox-backup
# Restore with token
$0 http://gitea.local:3000 admin secubox-backup abc123token
# Restore from specific branch
$0 -b dev http://gitea.local:3000 admin secubox-backup
Quick Bootstrap (paste into new OpenWrt shell):
wget -qO- http://your-gitea/user/repo/raw/master/bootstrap.sh | sh
EOF
}
# Parse arguments
BRANCH="master"
INCLUDE_NETWORK=0
while [ $# -gt 0 ]; do
case "$1" in
--interactive|-i)
interactive_mode
exit 0
;;
--branch|-b)
BRANCH="$2"
shift 2
;;
--include-network)
INCLUDE_NETWORK=1
shift
;;
--help|-h)
usage
exit 0
;;
-*)
log_error "Unknown option: $1"
usage
exit 1
;;
*)
break
;;
esac
done
# If no arguments, run interactive
if [ $# -eq 0 ]; then
interactive_mode
exit 0
fi
# Validate arguments
if [ $# -lt 3 ]; then
log_error "Missing required arguments"
usage
exit 1
fi
SERVER_URL="$1"
REPO_OWNER="$2"
REPO_NAME="$3"
ACCESS_TOKEN="${4:-}"
print_banner
echo ""
check_openwrt
install_deps
do_restore "$SERVER_URL" "$REPO_OWNER" "$REPO_NAME" "$ACCESS_TOKEN" "$BRANCH"

View File

@ -390,37 +390,202 @@ EOF
backup_dir="/tmp/secubox-gitea-backup-$$"
mkdir -p "$backup_dir"
# Collect configs
if [ "$(uci -q get secubox-p2p.gitea.include_configs)" = "1" ]; then
mkdir -p "$backup_dir/configs"
cp -r /etc/config/secubox* "$backup_dir/configs/" 2>/dev/null
cp -r /etc/config/network "$backup_dir/configs/" 2>/dev/null
cp -r /etc/config/firewall "$backup_dir/configs/" 2>/dev/null
cp -r /etc/config/wireless "$backup_dir/configs/" 2>/dev/null
# 1. UCI Configs - Core system configuration
mkdir -p "$backup_dir/configs"
for cfg in secubox secubox-appstore secubox-appstore-opkg secubox-exposure \
secubox-netifyd secubox-p2p secubox-core network firewall \
wireless dhcp system dropbear uhttpd haproxy crowdsec acme \
wireguard wireguard_* gitea nodogsplash; do
[ -f "/etc/config/$cfg" ] && cp "/etc/config/$cfg" "$backup_dir/configs/" 2>/dev/null
done
# 2. Profiles - Deployment templates
if [ -d "/usr/share/secubox/profiles" ]; then
mkdir -p "$backup_dir/profiles"
cp -r /usr/share/secubox/profiles/* "$backup_dir/profiles/" 2>/dev/null
fi
# Collect package list
if [ "$(uci -q get secubox-p2p.gitea.include_packages)" = "1" ]; then
mkdir -p "$backup_dir/packages"
opkg list-installed > "$backup_dir/packages/installed.txt" 2>/dev/null
# 3. Presets - Settings presets
if [ -d "/etc/secubox/presets" ]; then
mkdir -p "$backup_dir/presets"
cp -r /etc/secubox/presets/* "$backup_dir/presets/" 2>/dev/null
fi
# Collect scripts
if [ "$(uci -q get secubox-p2p.gitea.include_scripts)" = "1" ]; then
# 4. App Manifests - Plugin definitions
if [ -d "/usr/share/secubox/plugins" ]; then
mkdir -p "$backup_dir/manifests"
cp -r /usr/share/secubox/plugins/* "$backup_dir/manifests/" 2>/dev/null
fi
# 5. Scripts - Automation scripts
if [ -d "/etc/secubox/scripts" ]; then
mkdir -p "$backup_dir/scripts"
cp -r /etc/secubox/scripts/* "$backup_dir/scripts/" 2>/dev/null
fi
# Create manifest
# 6. Macros - Macro definitions
if [ -d "/etc/secubox/macros" ]; then
mkdir -p "$backup_dir/macros"
cp -r /etc/secubox/macros/* "$backup_dir/macros/" 2>/dev/null
fi
# 7. Workflows - Automation workflows
if [ -d "/etc/secubox/workflows" ]; then
mkdir -p "$backup_dir/workflows"
cp -r /etc/secubox/workflows/* "$backup_dir/workflows/" 2>/dev/null
fi
# 8. Package lists
mkdir -p "$backup_dir/packages"
opkg list-installed > "$backup_dir/packages/installed.txt" 2>/dev/null
# Also save SecuBox-specific package list
opkg list-installed | grep -E "^(secubox|luci-app-secubox)" > "$backup_dir/packages/secubox-packages.txt" 2>/dev/null
# 9. Service states
mkdir -p "$backup_dir/services"
for svc in /etc/init.d/secubox*; do
[ -x "$svc" ] && basename "$svc" >> "$backup_dir/services/enabled.txt"
done
# 10. Crontabs
if [ -f "/etc/crontabs/root" ]; then
mkdir -p "$backup_dir/cron"
cp /etc/crontabs/root "$backup_dir/cron/root" 2>/dev/null
fi
# 11. SSH authorized keys (for mesh access)
if [ -f "/etc/dropbear/authorized_keys" ]; then
mkdir -p "$backup_dir/ssh"
cp /etc/dropbear/authorized_keys "$backup_dir/ssh/" 2>/dev/null
fi
# 12. SSL Certificates (ACME)
if [ -d "/etc/acme" ]; then
mkdir -p "$backup_dir/certificates"
for cert in /etc/acme/*.cer; do
[ -f "$cert" ] && cp "$cert" "$backup_dir/certificates/" 2>/dev/null
done
fi
# 13. HAProxy configs
if [ -d "/etc/haproxy" ]; then
mkdir -p "$backup_dir/haproxy"
cp /etc/haproxy/*.cfg "$backup_dir/haproxy/" 2>/dev/null
fi
# 14. DNS/Hosts
mkdir -p "$backup_dir/dns"
[ -f "/etc/hosts" ] && cp /etc/hosts "$backup_dir/dns/" 2>/dev/null
[ -f "/etc/dnsmasq.conf" ] && cp /etc/dnsmasq.conf "$backup_dir/dns/" 2>/dev/null
[ -d "/etc/dnsmasq.d" ] && cp /etc/dnsmasq.d/* "$backup_dir/dns/" 2>/dev/null
# 15. Device info
mkdir -p "$backup_dir/device"
ubus call system board > "$backup_dir/device/board.json" 2>/dev/null
cat /proc/cpuinfo > "$backup_dir/device/cpuinfo.txt" 2>/dev/null
free -h > "$backup_dir/device/memory.txt" 2>/dev/null
# Create comprehensive manifest
config_count=$(find "$backup_dir/configs" -type f 2>/dev/null | wc -l)
profile_count=$(find "$backup_dir/profiles" -type f 2>/dev/null | wc -l)
manifest_count=$(find "$backup_dir/manifests" -type f 2>/dev/null | wc -l)
script_count=$(find "$backup_dir/scripts" -type f 2>/dev/null | wc -l)
cat > "$backup_dir/manifest.json" <<MANIFEST
{
"schema_version": "1.0",
"timestamp": "$(date -Iseconds)",
"hostname": "$(uci -q get system.@system[0].hostname || echo "secubox")",
"version": "$(cat /etc/secubox-version 2>/dev/null || echo "unknown")",
"message": "$message"
"secubox_version": "$(cat /etc/secubox-version 2>/dev/null || echo "unknown")",
"openwrt_version": "$(. /etc/openwrt_release 2>/dev/null && echo $DISTRIB_RELEASE || echo unknown)",
"device_model": "$(cat /tmp/sysinfo/model 2>/dev/null || echo "unknown")",
"message": "$message",
"contents": {
"configs": $config_count,
"profiles": $profile_count,
"manifests": $manifest_count,
"scripts": $script_count
},
"components": [
"configs",
"profiles",
"presets",
"manifests",
"scripts",
"macros",
"workflows",
"packages",
"services",
"cron",
"ssh",
"certificates",
"haproxy",
"dns",
"device"
]
}
MANIFEST
# Create bootstrap.sh for quick restore
cat > "$backup_dir/bootstrap.sh" <<'BOOTSTRAP'
#!/bin/sh
# SecuBox Quick Restore Bootstrap
# Run: wget -qO- URL | sh OR curl -sL URL | sh
set -e
SERVER_URL="__SERVER_URL__"
REPO_OWNER="__REPO_OWNER__"
REPO_NAME="__REPO_NAME__"
echo "SecuBox Quick Restore"
echo "====================="
echo "From: $SERVER_URL/$REPO_OWNER/$REPO_NAME"
echo ""
# Check OpenWrt
[ -f /etc/openwrt_release ] || { echo "Error: Not OpenWrt"; exit 1; }
# Install deps
opkg update >/dev/null 2>&1 || true
opkg install curl git-http 2>/dev/null || true
# Download restore script
RESTORE_URL="$SERVER_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/contents/scripts/secubox-restore?ref=master"
SCRIPT=$(curl -sL "$RESTORE_URL" | jsonfilter -e '@.content' 2>/dev/null | base64 -d 2>/dev/null)
if [ -n "$SCRIPT" ]; then
echo "$SCRIPT" > /tmp/secubox-restore
chmod +x /tmp/secubox-restore
/tmp/secubox-restore "$SERVER_URL" "$REPO_OWNER" "$REPO_NAME"
else
echo "Fallback: Direct config restore..."
# Minimal restore - fetch and apply configs
API_BASE="$SERVER_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/contents"
mkdir -p /tmp/secubox-restore-$$
# Get configs list
configs=$(curl -sL "$API_BASE/configs" | jsonfilter -e '@[*].name' 2>/dev/null)
for cfg in $configs; do
echo "Restoring: $cfg"
content=$(curl -sL "$API_BASE/configs/$cfg" | jsonfilter -e '@.content' 2>/dev/null | base64 -d)
[ -n "$content" ] && echo "$content" > "/etc/config/$cfg"
done
echo ""
echo "Basic restore complete. Reboot recommended."
fi
BOOTSTRAP
# Replace placeholders in bootstrap
sed -i "s|__SERVER_URL__|$server_url|g" "$backup_dir/bootstrap.sh"
sed -i "s|__REPO_OWNER__|$repo_owner|g" "$backup_dir/bootstrap.sh"
sed -i "s|__REPO_NAME__|$repo_name|g" "$backup_dir/bootstrap.sh"
# Also copy the full restore script
mkdir -p "$backup_dir/scripts"
if [ -f "/usr/bin/secubox-restore" ]; then
cp /usr/bin/secubox-restore "$backup_dir/scripts/"
fi
# Push each file via Gitea API
pushed_files=0
api_base="${server_url}/api/v1/repos/${repo_owner}/${repo_name}/contents"
@ -429,24 +594,24 @@ MANIFEST
rel_path="${file#$backup_dir/}"
content=$(base64 "$file" | tr -d '\n')
# Check if file exists (to update vs create)
existing=$(curl -s "${api_base}/${rel_path}" \
# Check if file exists (to update vs create) - use main branch
existing=$(curl -s "${api_base}/${rel_path}?ref=main" \
-H "Authorization: token $access_token" 2>/dev/null)
sha=$(echo "$existing" | jsonfilter -e '@.sha' 2>/dev/null)
if [ -n "$sha" ]; then
# Update existing file
# Update existing file with branch specification
curl -s -X PUT "${api_base}/${rel_path}" \
-H "Authorization: token $access_token" \
-H "Content-Type: application/json" \
-d "{\"message\":\"$message\",\"content\":\"$content\",\"sha\":\"$sha\"}" \
-d "{\"message\":\"$message\",\"content\":\"$content\",\"sha\":\"$sha\",\"branch\":\"main\"}" \
>/dev/null 2>&1
else
# Create new file
# Create new file on main branch
curl -s -X POST "${api_base}/${rel_path}" \
-H "Authorization: token $access_token" \
-H "Content-Type: application/json" \
-d "{\"message\":\"$message\",\"content\":\"$content\"}" \
-d "{\"message\":\"$message\",\"content\":\"$content\",\"branch\":\"main\"}" \
>/dev/null 2>&1
fi
pushed_files=$((pushed_files + 1))