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:
parent
2687e863a6
commit
a68d8b638f
@ -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') : ''
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
@ -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"]
|
||||
},
|
||||
|
||||
@ -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' }, [
|
||||
|
||||
@ -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",
|
||||
|
||||
342
package/secubox/secubox-p2p/README.md
Normal file
342
package/secubox/secubox-p2p/README.md
Normal 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
|
||||
446
package/secubox/secubox-p2p/root/usr/bin/secubox-restore
Normal file
446
package/secubox/secubox-p2p/root/usr/bin/secubox-restore
Normal 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"
|
||||
@ -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))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user