feat: implement network mode switcher with rollback protection
Implémente un switcher de mode réseau complet avec 4 modes: - Router: NAT, DHCP server, firewall (default) - Access Point: Bridge mode, no NAT, DHCP client - Repeater: WiFi client + AP relay with optimizations - Bridge: Pure L2 bridge, DHCP client Nouvelles méthodes RPCD: - get_current_mode: Détails du mode actif avec statut rollback - get_available_modes: Liste des modes avec features - set_mode: Préparer le changement de mode - preview_changes: Prévisualiser les modifications - apply_mode: Appliquer avec reconfiguration réseau complète - confirm_mode: Confirmer et annuler le timer de rollback - rollback: Restaurer la configuration précédente Sécurité: - Backup automatique avant changement - Rollback automatique après 2 minutes sans confirmation - Timer affiché en temps réel dans l'interface - Restauration complète de network/wireless/firewall/dhcp Vue wizard.js: - Cards interactives pour chaque mode avec icônes - Preview des changements avant application - Progress bar et instructions post-switch - Polling du timer de rollback - Boutons de confirmation et rollback manuel ACL mis à jour pour toutes les nouvelles méthodes.
This commit is contained in:
parent
5bd25d9b8e
commit
4e23037a22
@ -0,0 +1,410 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require dom';
|
||||
'require poll';
|
||||
|
||||
var callGetAvailableModes = rpc.declare({
|
||||
object: 'luci.network_modes',
|
||||
method: 'get_available_modes',
|
||||
expect: { modes: [] }
|
||||
});
|
||||
|
||||
var callGetCurrentMode = rpc.declare({
|
||||
object: 'luci.network_modes',
|
||||
method: 'get_current_mode',
|
||||
expect: { }
|
||||
});
|
||||
|
||||
var callSetMode = rpc.declare({
|
||||
object: 'luci.network_modes',
|
||||
method: 'set_mode',
|
||||
params: ['mode'],
|
||||
expect: { }
|
||||
});
|
||||
|
||||
var callPreviewChanges = rpc.declare({
|
||||
object: 'luci.network_modes',
|
||||
method: 'preview_changes',
|
||||
expect: { }
|
||||
});
|
||||
|
||||
var callApplyMode = rpc.declare({
|
||||
object: 'luci.network_modes',
|
||||
method: 'apply_mode',
|
||||
expect: { }
|
||||
});
|
||||
|
||||
var callConfirmMode = rpc.declare({
|
||||
object: 'luci.network_modes',
|
||||
method: 'confirm_mode',
|
||||
expect: { }
|
||||
});
|
||||
|
||||
var callRollback = rpc.declare({
|
||||
object: 'luci.network_modes',
|
||||
method: 'rollback',
|
||||
expect: { }
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
callGetAvailableModes(),
|
||||
callGetCurrentMode()
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var modes = data[0].modes || [];
|
||||
var currentModeData = data[1] || {};
|
||||
|
||||
var container = E('div', { 'class': 'cbi-map' });
|
||||
|
||||
// Header
|
||||
container.appendChild(E('h2', {}, _('Network Mode Switcher')));
|
||||
container.appendChild(E('div', { 'class': 'cbi-section-descr' },
|
||||
_('Sélectionnez et basculez entre différents modes réseau. Un rollback automatique de 2 minutes protège contre les configurations défectueuses.')
|
||||
));
|
||||
|
||||
// Current mode status
|
||||
if (currentModeData.rollback_active) {
|
||||
var remaining = currentModeData.rollback_remaining || 0;
|
||||
container.appendChild(this.renderRollbackBanner(remaining, currentModeData.current_mode));
|
||||
} else {
|
||||
container.appendChild(this.renderCurrentMode(currentModeData));
|
||||
}
|
||||
|
||||
// Modes grid
|
||||
container.appendChild(this.renderModesGrid(modes, currentModeData.current_mode));
|
||||
|
||||
// Instructions
|
||||
container.appendChild(this.renderInstructions());
|
||||
|
||||
// Start polling if rollback is active
|
||||
if (currentModeData.rollback_active) {
|
||||
this.startRollbackPoll();
|
||||
}
|
||||
|
||||
return container;
|
||||
},
|
||||
|
||||
renderCurrentMode: function(data) {
|
||||
var section = E('div', {
|
||||
'class': 'cbi-section',
|
||||
'style': 'background: #1e293b; padding: 16px; border-radius: 8px; margin-bottom: 24px;'
|
||||
});
|
||||
|
||||
section.appendChild(E('h3', { 'style': 'margin: 0 0 8px 0; color: #f1f5f9;' }, _('Mode Actuel')));
|
||||
section.appendChild(E('div', { 'style': 'color: #94a3b8; font-size: 14px;' }, [
|
||||
E('strong', { 'style': 'color: #22c55e; font-size: 18px;' }, data.mode_name || data.current_mode),
|
||||
E('br'),
|
||||
E('span', {}, data.description || ''),
|
||||
E('br'),
|
||||
E('span', { 'style': 'font-size: 12px;' }, _('Dernière modification: ') + (data.last_change || 'Never'))
|
||||
]));
|
||||
|
||||
return section;
|
||||
},
|
||||
|
||||
renderRollbackBanner: function(remaining, mode) {
|
||||
var minutes = Math.floor(remaining / 60);
|
||||
var seconds = remaining % 60;
|
||||
var timeStr = minutes + 'm ' + seconds + 's';
|
||||
|
||||
var banner = E('div', {
|
||||
'class': 'alert-message warning',
|
||||
'style': 'background: #f59e0b; color: #000; padding: 16px; border-radius: 8px; margin-bottom: 24px;'
|
||||
}, [
|
||||
E('h3', { 'style': 'margin: 0 0 8px 0;' }, '⏱️ ' + _('Rollback Automatique Actif')),
|
||||
E('div', { 'id': 'rollback-timer', 'style': 'font-size: 20px; font-weight: bold; margin: 8px 0;' },
|
||||
_('Temps restant: ') + timeStr
|
||||
),
|
||||
E('div', { 'style': 'margin: 12px 0;' },
|
||||
_('Le mode ') + mode + _(' sera annulé automatiquement si vous ne confirmez pas.')
|
||||
),
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-positive',
|
||||
'style': 'margin-right: 8px;',
|
||||
'click': ui.createHandlerFn(this, 'handleConfirmMode')
|
||||
}, _('✓ Confirmer le Mode')),
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-negative',
|
||||
'click': ui.createHandlerFn(this, 'handleRollbackNow')
|
||||
}, _('↩ Annuler Maintenant'))
|
||||
]);
|
||||
|
||||
return banner;
|
||||
},
|
||||
|
||||
renderModesGrid: function(modes, currentMode) {
|
||||
var grid = E('div', {
|
||||
'class': 'cbi-section',
|
||||
'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; margin-bottom: 24px;'
|
||||
});
|
||||
|
||||
modes.forEach(L.bind(function(mode) {
|
||||
grid.appendChild(this.renderModeCard(mode, mode.current));
|
||||
}, this));
|
||||
|
||||
return grid;
|
||||
},
|
||||
|
||||
renderModeCard: function(mode, isCurrent) {
|
||||
var borderColor = isCurrent ? '#22c55e' : '#334155';
|
||||
var bgColor = isCurrent ? '#1e293b' : '#0f172a';
|
||||
|
||||
var card = E('div', {
|
||||
'class': 'mode-card',
|
||||
'style': 'background: ' + bgColor + '; border: 2px solid ' + borderColor + '; border-radius: 8px; padding: 16px; cursor: ' + (isCurrent ? 'default' : 'pointer') + '; transition: all 0.2s;',
|
||||
'data-mode': mode.id
|
||||
});
|
||||
|
||||
// Icon and name
|
||||
card.appendChild(E('div', { 'style': 'font-size: 32px; margin-bottom: 8px;' }, mode.icon));
|
||||
card.appendChild(E('div', { 'style': 'font-size: 18px; font-weight: bold; color: #f1f5f9; margin-bottom: 4px;' },
|
||||
mode.name
|
||||
));
|
||||
|
||||
// Description
|
||||
card.appendChild(E('div', { 'style': 'color: #94a3b8; font-size: 14px; margin-bottom: 12px; min-height: 40px;' },
|
||||
mode.description
|
||||
));
|
||||
|
||||
// Features list
|
||||
var featuresList = E('ul', { 'style': 'color: #64748b; font-size: 13px; margin: 12px 0; padding-left: 20px;' });
|
||||
(mode.features || []).forEach(function(feature) {
|
||||
featuresList.appendChild(E('li', {}, feature));
|
||||
});
|
||||
card.appendChild(featuresList);
|
||||
|
||||
// Button
|
||||
if (isCurrent) {
|
||||
card.appendChild(E('div', {
|
||||
'class': 'cbi-value-description',
|
||||
'style': 'color: #22c55e; font-weight: bold; text-align: center; padding: 8px;'
|
||||
}, '✓ ' + _('Mode Actuel')));
|
||||
} else {
|
||||
var btn = E('button', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'style': 'width: 100%;',
|
||||
'click': ui.createHandlerFn(this, 'handleSwitchMode', mode)
|
||||
}, _('Switch to ') + mode.name);
|
||||
card.appendChild(btn);
|
||||
|
||||
// Hover effect
|
||||
card.addEventListener('mouseenter', function() {
|
||||
this.style.borderColor = '#3b82f6';
|
||||
this.style.transform = 'translateY(-2px)';
|
||||
});
|
||||
card.addEventListener('mouseleave', function() {
|
||||
this.style.borderColor = borderColor;
|
||||
this.style.transform = 'translateY(0)';
|
||||
});
|
||||
}
|
||||
|
||||
return card;
|
||||
},
|
||||
|
||||
renderInstructions: function() {
|
||||
var section = E('div', { 'class': 'cbi-section' });
|
||||
section.appendChild(E('h3', {}, _('Instructions')));
|
||||
|
||||
var steps = E('ol', { 'style': 'color: #94a3b8; line-height: 1.8;' });
|
||||
steps.appendChild(E('li', {}, _('Sélectionnez le mode réseau souhaité en cliquant sur "Switch to..."')));
|
||||
steps.appendChild(E('li', {}, _('Vérifiez les changements qui seront appliqués dans la prévisualisation')));
|
||||
steps.appendChild(E('li', {}, _('Confirmez l\'application - la configuration réseau sera modifiée')));
|
||||
steps.appendChild(E('li', {}, _('Reconnectez-vous via la nouvelle IP si nécessaire (notée dans les instructions)')));
|
||||
steps.appendChild(E('li', {}, _('Confirmez le nouveau mode dans les 2 minutes, sinon rollback automatique')));
|
||||
|
||||
section.appendChild(steps);
|
||||
|
||||
return section;
|
||||
},
|
||||
|
||||
handleSwitchMode: function(mode, ev) {
|
||||
var modal = ui.showModal(_('Switch to ') + mode.name, [
|
||||
E('p', { 'class': 'spinning' }, _('Préparation du changement de mode...'))
|
||||
]);
|
||||
|
||||
return callSetMode(mode.id).then(L.bind(function(result) {
|
||||
if (!result.success) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', result.error || _('Erreur')), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show preview
|
||||
return callPreviewChanges().then(L.bind(function(preview) {
|
||||
this.showPreviewModal(mode, preview);
|
||||
}, this));
|
||||
}, this)).catch(function(err) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Erreur: ') + err.message), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
showPreviewModal: function(mode, preview) {
|
||||
if (!preview.success) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', preview.error || _('Erreur')), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
var content = [
|
||||
E('h4', {}, _('Changements qui seront appliqués:')),
|
||||
E('div', { 'style': 'background: #1e293b; padding: 12px; border-radius: 4px; margin: 12px 0;' }, [
|
||||
E('strong', {}, preview.current_mode + ' → ' + preview.target_mode)
|
||||
])
|
||||
];
|
||||
|
||||
// Changes list
|
||||
var changesList = E('ul', { 'style': 'margin: 12px 0;' });
|
||||
(preview.changes || []).forEach(function(change) {
|
||||
changesList.appendChild(E('li', {}, [
|
||||
E('strong', {}, change.file + ': '),
|
||||
E('span', {}, change.change)
|
||||
]));
|
||||
});
|
||||
content.push(changesList);
|
||||
|
||||
// Warnings
|
||||
if (preview.warnings && preview.warnings.length > 0) {
|
||||
content.push(E('div', {
|
||||
'class': 'alert-message warning',
|
||||
'style': 'background: #f59e0b20; border-left: 4px solid #f59e0b; padding: 12px; margin: 12px 0;'
|
||||
}, [
|
||||
E('h5', { 'style': 'margin: 0 0 8px 0;' }, '⚠️ ' + _('Avertissements:')),
|
||||
E('ul', { 'style': 'margin: 0; padding-left: 20px;' },
|
||||
preview.warnings.map(function(w) {
|
||||
return E('li', {}, w);
|
||||
})
|
||||
)
|
||||
]));
|
||||
}
|
||||
|
||||
// Buttons
|
||||
content.push(E('div', { 'class': 'right', 'style': 'margin-top: 16px;' }, [
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-neutral',
|
||||
'click': ui.hideModal
|
||||
}, _('Annuler')),
|
||||
' ',
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-positive',
|
||||
'click': L.bind(this.handleApplyMode, this, mode)
|
||||
}, _('Appliquer le Mode'))
|
||||
]));
|
||||
|
||||
ui.showModal(_('Prévisualisation: ') + mode.name, content);
|
||||
},
|
||||
|
||||
handleApplyMode: function(mode, ev) {
|
||||
ui.showModal(_('Application en cours...'), [
|
||||
E('p', { 'class': 'spinning' }, _('Application du mode ') + mode.name + '...'),
|
||||
E('p', {}, _('La connexion réseau sera brièvement interrompue.'))
|
||||
]);
|
||||
|
||||
return callApplyMode().then(function(result) {
|
||||
if (!result.success) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', result.error || _('Erreur')), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
ui.hideModal();
|
||||
|
||||
// Show success with instructions
|
||||
ui.showModal(_('Mode Appliqué'), [
|
||||
E('div', { 'class': 'alert-message success' }, [
|
||||
E('h4', {}, '✓ ' + _('Mode ') + mode.name + _(' activé')),
|
||||
E('p', {}, _('Rollback automatique dans 2 minutes si non confirmé.')),
|
||||
E('p', {}, _('Si vous perdez la connexion:')),
|
||||
E('ul', {}, [
|
||||
E('li', {}, _('Router: http://192.168.1.1')),
|
||||
E('li', {}, _('Access Point/Bridge: Utilisez DHCP')),
|
||||
E('li', {}, _('Repeater: http://192.168.2.1'))
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'right', 'style': 'margin-top: 16px;' }, [
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-positive',
|
||||
'click': function() {
|
||||
ui.hideModal();
|
||||
window.location.reload();
|
||||
}
|
||||
}, _('Recharger la Page'))
|
||||
])
|
||||
]);
|
||||
|
||||
}).catch(function(err) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Erreur: ') + err.message), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
handleConfirmMode: function(ev) {
|
||||
return callConfirmMode().then(function(result) {
|
||||
if (result.success) {
|
||||
ui.addNotification(null, E('p', '✓ ' + result.message), 'info');
|
||||
setTimeout(function() {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
ui.addNotification(null, E('p', result.error), 'error');
|
||||
}
|
||||
}).catch(function(err) {
|
||||
ui.addNotification(null, E('p', _('Erreur: ') + err.message), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
handleRollbackNow: function(ev) {
|
||||
if (!confirm(_('Annuler le changement de mode et revenir à la configuration précédente?'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
ui.showModal(_('Rollback...'), [
|
||||
E('p', { 'class': 'spinning' }, _('Restauration de la configuration précédente...'))
|
||||
]);
|
||||
|
||||
return callRollback().then(function(result) {
|
||||
ui.hideModal();
|
||||
if (result.success) {
|
||||
ui.addNotification(null, E('p', '✓ ' + result.message), 'info');
|
||||
setTimeout(function() {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
ui.addNotification(null, E('p', result.error), 'error');
|
||||
}
|
||||
}).catch(function(err) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Erreur: ') + err.message), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
startRollbackPoll: function() {
|
||||
poll.add(L.bind(function() {
|
||||
return callGetCurrentMode().then(L.bind(function(data) {
|
||||
if (!data.rollback_active) {
|
||||
poll.stop();
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
var timerElem = document.getElementById('rollback-timer');
|
||||
if (timerElem) {
|
||||
var remaining = data.rollback_remaining || 0;
|
||||
var minutes = Math.floor(remaining / 60);
|
||||
var seconds = remaining % 60;
|
||||
timerElem.textContent = _('Temps restant: ') + minutes + 'm ' + seconds + 's';
|
||||
}
|
||||
}, this));
|
||||
}, this), 1);
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@ -359,41 +359,175 @@ get_router_config() {
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Apply mode change
|
||||
# Apply mode change (with actual network reconfiguration and rollback timer)
|
||||
apply_mode() {
|
||||
read input
|
||||
json_load "$input"
|
||||
json_get_var mode mode
|
||||
|
||||
json_init
|
||||
|
||||
# Validate mode
|
||||
case "$mode" in
|
||||
sniffer|accesspoint|relay|router)
|
||||
;;
|
||||
*)
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "Invalid mode: $mode"
|
||||
json_dump
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
local pending_mode=$(uci -q get network-modes.config.pending_mode || echo "")
|
||||
|
||||
if [ -z "$pending_mode" ]; then
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "Aucun mode en attente. Utilisez set_mode d'abord."
|
||||
json_dump
|
||||
return
|
||||
fi
|
||||
|
||||
local current_mode=$(uci -q get network-modes.config.current_mode || echo "router")
|
||||
|
||||
# Backup current config
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
local backup_file="$BACKUP_DIR/backup_$(date +%Y%m%d_%H%M%S).tar.gz"
|
||||
tar -czf "$backup_file" /etc/config/network /etc/config/wireless /etc/config/firewall /etc/config/dhcp 2>/dev/null
|
||||
|
||||
|
||||
# Apply network configuration based on mode
|
||||
case "$pending_mode" in
|
||||
router)
|
||||
# Router mode: NAT, DHCP server, firewall
|
||||
# WAN interface
|
||||
uci delete network.wan 2>/dev/null
|
||||
uci set network.wan=interface
|
||||
uci set network.wan.proto='dhcp'
|
||||
uci set network.wan.device='eth1'
|
||||
|
||||
# LAN interface
|
||||
uci set network.lan=interface
|
||||
uci set network.lan.proto='static'
|
||||
uci set network.lan.device='eth0'
|
||||
uci set network.lan.ipaddr='192.168.1.1'
|
||||
uci set network.lan.netmask='255.255.255.0'
|
||||
|
||||
# DHCP server
|
||||
uci set dhcp.lan=dhcp
|
||||
uci set dhcp.lan.interface='lan'
|
||||
uci set dhcp.lan.start='100'
|
||||
uci set dhcp.lan.limit='150'
|
||||
uci set dhcp.lan.leasetime='12h'
|
||||
|
||||
# Firewall zones
|
||||
uci set firewall.@zone[0]=zone
|
||||
uci set firewall.@zone[0].name='lan'
|
||||
uci set firewall.@zone[0].input='ACCEPT'
|
||||
uci set firewall.@zone[0].output='ACCEPT'
|
||||
uci set firewall.@zone[0].forward='ACCEPT'
|
||||
|
||||
uci set firewall.@zone[1]=zone
|
||||
uci set firewall.@zone[1].name='wan'
|
||||
uci set firewall.@zone[1].input='REJECT'
|
||||
uci set firewall.@zone[1].output='ACCEPT'
|
||||
uci set firewall.@zone[1].forward='REJECT'
|
||||
uci set firewall.@zone[1].masq='1'
|
||||
uci set firewall.@zone[1].mtu_fix='1'
|
||||
;;
|
||||
|
||||
accesspoint)
|
||||
# Access Point mode: Bridge, no NAT, DHCP client
|
||||
# Delete WAN
|
||||
uci delete network.wan 2>/dev/null
|
||||
|
||||
# Bridge LAN
|
||||
uci set network.lan=interface
|
||||
uci set network.lan.proto='dhcp'
|
||||
uci set network.lan.type='bridge'
|
||||
uci set network.lan.ifname='eth0 eth1'
|
||||
|
||||
# Disable DHCP server
|
||||
uci set dhcp.lan.ignore='1'
|
||||
|
||||
# Disable firewall
|
||||
uci set firewall.@zone[0].input='ACCEPT'
|
||||
uci set firewall.@zone[0].forward='ACCEPT'
|
||||
uci delete firewall.@zone[1] 2>/dev/null
|
||||
;;
|
||||
|
||||
relay)
|
||||
# Repeater mode: STA + AP relay
|
||||
# Client interface (sta)
|
||||
uci set network.wwan=interface
|
||||
uci set network.wwan.proto='dhcp'
|
||||
|
||||
# AP interface
|
||||
uci set network.lan=interface
|
||||
uci set network.lan.proto='static'
|
||||
uci set network.lan.ipaddr='192.168.2.1'
|
||||
uci set network.lan.netmask='255.255.255.0'
|
||||
|
||||
# Relay with relayd
|
||||
uci set network.stabridge=interface
|
||||
uci set network.stabridge.proto='relay'
|
||||
uci set network.stabridge.network='lan wwan'
|
||||
;;
|
||||
|
||||
bridge)
|
||||
# Pure L2 bridge: all interfaces bridged, DHCP client
|
||||
uci delete network.wan 2>/dev/null
|
||||
|
||||
# Bridge all interfaces
|
||||
uci set network.lan=interface
|
||||
uci set network.lan.proto='dhcp'
|
||||
uci set network.lan.type='bridge'
|
||||
uci set network.lan.bridge_empty='1'
|
||||
|
||||
# Disable DHCP server
|
||||
uci set dhcp.lan.ignore='1'
|
||||
|
||||
# Disable firewall
|
||||
uci delete firewall.@zone[1] 2>/dev/null
|
||||
;;
|
||||
esac
|
||||
|
||||
# Commit all changes
|
||||
uci commit network
|
||||
uci commit dhcp
|
||||
uci commit firewall
|
||||
|
||||
# Update current mode
|
||||
uci set network-modes.config.current_mode="$mode"
|
||||
uci set network-modes.config.current_mode="$pending_mode"
|
||||
uci set network-modes.config.last_change="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
uci set network-modes.config.rollback_timer="120"
|
||||
uci commit network-modes
|
||||
|
||||
|
||||
# Start rollback timer (2 minutes) in background
|
||||
(
|
||||
for i in $(seq 120 -1 0); do
|
||||
echo "$i" > /tmp/network-mode-rollback.remaining
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Timer expired, rollback
|
||||
logger -t network-modes "Rollback timer expired, reverting to $current_mode"
|
||||
|
||||
cd /
|
||||
tar -xzf "$backup_file" 2>/dev/null
|
||||
/etc/init.d/network reload 2>&1
|
||||
/etc/init.d/firewall reload 2>&1
|
||||
/etc/init.d/dnsmasq reload 2>&1
|
||||
|
||||
uci set network-modes.config.current_mode="$current_mode"
|
||||
uci delete network-modes.config.pending_mode 2>/dev/null
|
||||
uci set network-modes.config.last_change="$(date '+%Y-%m-%d %H:%M:%S') (auto-rollback)"
|
||||
uci commit network-modes
|
||||
|
||||
rm -f /tmp/network-mode-rollback.pid
|
||||
rm -f /tmp/network-mode-rollback.remaining
|
||||
) &
|
||||
|
||||
echo $! > /tmp/network-mode-rollback.pid
|
||||
|
||||
# Apply network changes NOW (async to not block RPC)
|
||||
(
|
||||
sleep 2
|
||||
/etc/init.d/network reload 2>&1
|
||||
/etc/init.d/firewall reload 2>&1
|
||||
/etc/init.d/dnsmasq reload 2>&1
|
||||
) &
|
||||
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "mode" "$mode"
|
||||
json_add_string "message" "Mode changed to $mode. Please reboot or apply network changes."
|
||||
json_add_string "mode" "$pending_mode"
|
||||
json_add_string "previous_mode" "$current_mode"
|
||||
json_add_string "message" "Mode $pending_mode appliqué. Confirmez dans les 2 minutes ou rollback automatique."
|
||||
json_add_string "backup" "$backup_file"
|
||||
|
||||
json_add_int "rollback_seconds" 120
|
||||
|
||||
json_dump
|
||||
}
|
||||
|
||||
@ -653,10 +787,320 @@ config forwarding
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Get current mode details
|
||||
get_current_mode() {
|
||||
json_init
|
||||
|
||||
local current_mode=$(uci -q get network-modes.config.current_mode || echo "router")
|
||||
local last_change=$(uci -q get network-modes.config.last_change || echo "Never")
|
||||
local pending_mode=$(uci -q get network-modes.config.pending_mode || echo "")
|
||||
local rollback_timer=$(uci -q get network-modes.config.rollback_timer || echo "0")
|
||||
|
||||
json_add_string "current_mode" "$current_mode"
|
||||
json_add_string "mode_name" "$(uci -q get network-modes.$current_mode.name || echo "$current_mode")"
|
||||
json_add_string "description" "$(uci -q get network-modes.$current_mode.description || echo "")"
|
||||
json_add_string "last_change" "$last_change"
|
||||
json_add_string "pending_mode" "$pending_mode"
|
||||
json_add_int "rollback_timer" "$rollback_timer"
|
||||
|
||||
# Check if rollback is active
|
||||
if [ -f "/tmp/network-mode-rollback.pid" ]; then
|
||||
json_add_boolean "rollback_active" 1
|
||||
local remaining=$(cat /tmp/network-mode-rollback.remaining 2>/dev/null || echo "0")
|
||||
json_add_int "rollback_remaining" "$remaining"
|
||||
else
|
||||
json_add_boolean "rollback_active" 0
|
||||
json_add_int "rollback_remaining" 0
|
||||
fi
|
||||
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Get available modes
|
||||
get_available_modes() {
|
||||
json_init
|
||||
json_add_array "modes"
|
||||
|
||||
local current_mode=$(uci -q get network-modes.config.current_mode || echo "router")
|
||||
|
||||
# Router mode
|
||||
json_add_object
|
||||
json_add_string "id" "router"
|
||||
json_add_string "name" "Router"
|
||||
json_add_string "description" "Mode routeur complet avec NAT, DHCP et firewall"
|
||||
json_add_string "icon" "🌐"
|
||||
json_add_boolean "current" "$([ "$current_mode" = "router" ] && echo 1 || echo 0)"
|
||||
json_add_array "features"
|
||||
json_add_string "" "NAT activé"
|
||||
json_add_string "" "Serveur DHCP"
|
||||
json_add_string "" "Firewall (zones WAN/LAN)"
|
||||
json_add_string "" "Proxy optionnel"
|
||||
json_close_array
|
||||
json_close_object
|
||||
|
||||
# Access Point mode
|
||||
json_add_object
|
||||
json_add_string "id" "accesspoint"
|
||||
json_add_string "name" "Access Point"
|
||||
json_add_string "description" "Point d'accès WiFi en mode bridge, pas de NAT"
|
||||
json_add_string "icon" "📶"
|
||||
json_add_boolean "current" "$([ "$current_mode" = "accesspoint" ] && echo 1 || echo 0)"
|
||||
json_add_array "features"
|
||||
json_add_string "" "Bridge WAN+LAN"
|
||||
json_add_string "" "Pas de DHCP (mode client)"
|
||||
json_add_string "" "Pas de firewall"
|
||||
json_add_string "" "Optimisations WiFi 802.11r/k/v"
|
||||
json_close_array
|
||||
json_close_object
|
||||
|
||||
# Repeater/Relay mode
|
||||
json_add_object
|
||||
json_add_string "id" "relay"
|
||||
json_add_string "name" "Repeater"
|
||||
json_add_string "description" "Client WiFi + Répéteur AP avec optimisations"
|
||||
json_add_string "icon" "🔄"
|
||||
json_add_boolean "current" "$([ "$current_mode" = "relay" ] && echo 1 || echo 0)"
|
||||
json_add_array "features"
|
||||
json_add_string "" "Client WiFi (sta0)"
|
||||
json_add_string "" "AP répéteur (ap0)"
|
||||
json_add_string "" "WireGuard optionnel"
|
||||
json_add_string "" "Optimisations MTU/MSS"
|
||||
json_close_array
|
||||
json_close_object
|
||||
|
||||
# Bridge mode
|
||||
json_add_object
|
||||
json_add_string "id" "bridge"
|
||||
json_add_string "name" "Bridge"
|
||||
json_add_string "description" "Bridge Layer 2 pur, DHCP client"
|
||||
json_add_string "icon" "🔗"
|
||||
json_add_boolean "current" "$([ "$current_mode" = "bridge" ] && echo 1 || echo 0)"
|
||||
json_add_array "features"
|
||||
json_add_string "" "Bridge transparent L2"
|
||||
json_add_string "" "Toutes interfaces bridgées"
|
||||
json_add_string "" "DHCP client"
|
||||
json_add_string "" "Pas de firewall"
|
||||
json_close_array
|
||||
json_close_object
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Set mode (prepare for switch)
|
||||
set_mode() {
|
||||
read input
|
||||
json_load "$input"
|
||||
json_get_var target_mode mode
|
||||
|
||||
json_init
|
||||
|
||||
# Validate mode
|
||||
case "$target_mode" in
|
||||
router|accesspoint|relay|bridge)
|
||||
;;
|
||||
*)
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "Mode invalide: $target_mode"
|
||||
json_dump
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
local current_mode=$(uci -q get network-modes.config.current_mode || echo "router")
|
||||
|
||||
if [ "$current_mode" = "$target_mode" ]; then
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "Déjà en mode $target_mode"
|
||||
json_dump
|
||||
return
|
||||
fi
|
||||
|
||||
# Store pending mode
|
||||
uci set network-modes.config.pending_mode="$target_mode"
|
||||
uci set network-modes.config.pending_since="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
uci commit network-modes
|
||||
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "current_mode" "$current_mode"
|
||||
json_add_string "target_mode" "$target_mode"
|
||||
json_add_string "message" "Mode $target_mode préparé. Utilisez preview_changes puis apply_mode."
|
||||
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Preview changes before applying
|
||||
preview_changes() {
|
||||
json_init
|
||||
|
||||
local current_mode=$(uci -q get network-modes.config.current_mode || echo "router")
|
||||
local pending_mode=$(uci -q get network-modes.config.pending_mode || echo "")
|
||||
|
||||
if [ -z "$pending_mode" ]; then
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "Aucun changement de mode en attente"
|
||||
json_dump
|
||||
return
|
||||
fi
|
||||
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "current_mode" "$current_mode"
|
||||
json_add_string "target_mode" "$pending_mode"
|
||||
|
||||
# Changes array
|
||||
json_add_array "changes"
|
||||
|
||||
case "$pending_mode" in
|
||||
router)
|
||||
json_add_object
|
||||
json_add_string "file" "/etc/config/network"
|
||||
json_add_string "change" "WAN: proto dhcp, NAT enabled"
|
||||
json_close_object
|
||||
json_add_object
|
||||
json_add_string "file" "/etc/config/network"
|
||||
json_add_string "change" "LAN: static IP, DHCP server enabled"
|
||||
json_close_object
|
||||
json_add_object
|
||||
json_add_string "file" "/etc/config/firewall"
|
||||
json_add_string "change" "Zones: WAN, LAN, forwarding rules"
|
||||
json_close_object
|
||||
;;
|
||||
accesspoint)
|
||||
json_add_object
|
||||
json_add_string "file" "/etc/config/network"
|
||||
json_add_string "change" "Bridge: br-lan (WAN+LAN)"
|
||||
json_close_object
|
||||
json_add_object
|
||||
json_add_string "file" "/etc/config/network"
|
||||
json_add_string "change" "DHCP: client mode"
|
||||
json_close_object
|
||||
json_add_object
|
||||
json_add_string "file" "/etc/config/firewall"
|
||||
json_add_string "change" "Firewall: disabled"
|
||||
json_close_object
|
||||
;;
|
||||
relay)
|
||||
json_add_object
|
||||
json_add_string "file" "/etc/config/wireless"
|
||||
json_add_string "change" "WiFi STA: client mode on wlan0"
|
||||
json_close_object
|
||||
json_add_object
|
||||
json_add_string "file" "/etc/config/wireless"
|
||||
json_add_string "change" "WiFi AP: repeater mode on wlan1"
|
||||
json_close_object
|
||||
json_add_object
|
||||
json_add_string "file" "/etc/config/network"
|
||||
json_add_string "change" "Relay: relayd between sta0 and ap0"
|
||||
json_close_object
|
||||
;;
|
||||
bridge)
|
||||
json_add_object
|
||||
json_add_string "file" "/etc/config/network"
|
||||
json_add_string "change" "Bridge: all interfaces to br-lan"
|
||||
json_close_object
|
||||
json_add_object
|
||||
json_add_string "file" "/etc/config/network"
|
||||
json_add_string "change" "DHCP: client only"
|
||||
json_close_object
|
||||
json_add_object
|
||||
json_add_string "file" "/etc/config/firewall"
|
||||
json_add_string "change" "Firewall: disabled"
|
||||
json_close_object
|
||||
;;
|
||||
esac
|
||||
|
||||
json_close_array
|
||||
|
||||
# Warnings
|
||||
json_add_array "warnings"
|
||||
json_add_string "" "La connexion réseau sera interrompue pendant la reconfiguration"
|
||||
json_add_string "" "Vous devrez peut-être reconnecter via la nouvelle IP"
|
||||
json_add_string "" "Un rollback automatique de 2 minutes sera activé"
|
||||
json_add_string "" "Vous devez confirmer le changement avant expiration"
|
||||
json_close_array
|
||||
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Confirm mode change (cancel rollback timer)
|
||||
confirm_mode() {
|
||||
json_init
|
||||
|
||||
local current_mode=$(uci -q get network-modes.config.current_mode || echo "router")
|
||||
|
||||
# Stop rollback timer
|
||||
if [ -f "/tmp/network-mode-rollback.pid" ]; then
|
||||
local pid=$(cat /tmp/network-mode-rollback.pid)
|
||||
kill $pid 2>/dev/null
|
||||
rm -f /tmp/network-mode-rollback.pid
|
||||
rm -f /tmp/network-mode-rollback.remaining
|
||||
|
||||
# Clear pending mode
|
||||
uci delete network-modes.config.pending_mode 2>/dev/null
|
||||
uci delete network-modes.config.pending_since 2>/dev/null
|
||||
uci set network-modes.config.rollback_timer="0"
|
||||
uci commit network-modes
|
||||
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "Mode $current_mode confirmé, rollback annulé"
|
||||
json_add_string "mode" "$current_mode"
|
||||
else
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "Aucun rollback actif"
|
||||
fi
|
||||
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Rollback to previous mode
|
||||
rollback() {
|
||||
json_init
|
||||
|
||||
# Stop any active rollback timer
|
||||
if [ -f "/tmp/network-mode-rollback.pid" ]; then
|
||||
local pid=$(cat /tmp/network-mode-rollback.pid)
|
||||
kill $pid 2>/dev/null
|
||||
rm -f /tmp/network-mode-rollback.pid
|
||||
rm -f /tmp/network-mode-rollback.remaining
|
||||
fi
|
||||
|
||||
# Get backup file
|
||||
local latest_backup=$(ls -t "$BACKUP_DIR"/backup_*.tar.gz 2>/dev/null | head -1)
|
||||
|
||||
if [ -z "$latest_backup" ]; then
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "Aucune sauvegarde disponible"
|
||||
json_dump
|
||||
return
|
||||
fi
|
||||
|
||||
# Restore backup
|
||||
cd /
|
||||
tar -xzf "$latest_backup" 2>/dev/null
|
||||
|
||||
# Reload network services
|
||||
/etc/init.d/network reload 2>&1
|
||||
/etc/init.d/firewall reload 2>&1
|
||||
/etc/init.d/dnsmasq reload 2>&1
|
||||
|
||||
# Update UCI
|
||||
local previous_mode=$(uci -q get network-modes.config.current_mode || echo "router")
|
||||
uci delete network-modes.config.pending_mode 2>/dev/null
|
||||
uci set network-modes.config.last_change="$(date '+%Y-%m-%d %H:%M:%S') (rollback)"
|
||||
uci commit network-modes
|
||||
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "Configuration restaurée depuis la sauvegarde"
|
||||
json_add_string "mode" "$previous_mode"
|
||||
json_add_string "backup_file" "$latest_backup"
|
||||
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Main dispatcher
|
||||
case "$1" in
|
||||
list)
|
||||
echo '{"status":{},"modes":{},"sniffer_config":{},"ap_config":{},"relay_config":{},"router_config":{},"apply_mode":{"mode":"str"},"update_settings":{"mode":"str"},"add_vhost":{"domain":"str","backend":"str","port":"int","ssl":"bool"},"generate_config":{"mode":"str"}}'
|
||||
echo '{"status":{},"modes":{},"get_current_mode":{},"get_available_modes":{},"set_mode":{"mode":"str"},"preview_changes":{},"apply_mode":{},"confirm_mode":{},"rollback":{},"sniffer_config":{},"ap_config":{},"relay_config":{},"router_config":{},"update_settings":{"mode":"str"},"add_vhost":{"domain":"str","backend":"str","port":"int","ssl":"bool"},"generate_config":{"mode":"str"}}'
|
||||
;;
|
||||
call)
|
||||
case "$2" in
|
||||
@ -666,6 +1110,27 @@ case "$1" in
|
||||
modes)
|
||||
get_modes
|
||||
;;
|
||||
get_current_mode)
|
||||
get_current_mode
|
||||
;;
|
||||
get_available_modes)
|
||||
get_available_modes
|
||||
;;
|
||||
set_mode)
|
||||
set_mode
|
||||
;;
|
||||
preview_changes)
|
||||
preview_changes
|
||||
;;
|
||||
apply_mode)
|
||||
apply_mode
|
||||
;;
|
||||
confirm_mode)
|
||||
confirm_mode
|
||||
;;
|
||||
rollback)
|
||||
rollback
|
||||
;;
|
||||
sniffer_config)
|
||||
get_sniffer_config
|
||||
;;
|
||||
@ -678,9 +1143,6 @@ case "$1" in
|
||||
router_config)
|
||||
get_router_config
|
||||
;;
|
||||
apply_mode)
|
||||
apply_mode
|
||||
;;
|
||||
update_settings)
|
||||
update_settings
|
||||
;;
|
||||
|
||||
@ -3,7 +3,18 @@
|
||||
"description": "Grant access to LuCI Network Modes Dashboard",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"network-modes": [ "status", "modes", "sniffer_config", "ap_config", "relay_config", "router_config", "generate_config" ],
|
||||
"network-modes": [
|
||||
"status",
|
||||
"modes",
|
||||
"get_current_mode",
|
||||
"get_available_modes",
|
||||
"preview_changes",
|
||||
"sniffer_config",
|
||||
"ap_config",
|
||||
"relay_config",
|
||||
"router_config",
|
||||
"generate_config"
|
||||
],
|
||||
"system": [ "info", "board" ],
|
||||
"network.interface": [ "status", "dump" ],
|
||||
"iwinfo": [ "info", "scan" ],
|
||||
@ -17,9 +28,16 @@
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"network-modes": [ "apply_mode", "update_settings", "add_vhost" ]
|
||||
"network-modes": [
|
||||
"set_mode",
|
||||
"apply_mode",
|
||||
"confirm_mode",
|
||||
"rollback",
|
||||
"update_settings",
|
||||
"add_vhost"
|
||||
]
|
||||
},
|
||||
"uci": [ "network-modes" ]
|
||||
"uci": [ "network", "wireless", "firewall", "dhcp", "network-modes" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user