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:
CyberMind-FR 2025-12-24 00:45:19 +01:00
parent 5bd25d9b8e
commit 4e23037a22
3 changed files with 921 additions and 31 deletions

View File

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

View File

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

View File

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