From 562ac55fe13fedc048c212b62918a6cd452805a2 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Sun, 28 Dec 2025 14:25:50 +0100 Subject: [PATCH] feat(network-modes): Prepare v0.3.5 implementation foundation - Version bump to 0.3.5 in Makefile and README - Add helpers.js utility module for common UI operations - Expand API with new RPC method declarations - Enhance view files with improved functionality: - accesspoint.js: Enhanced WiFi configuration options - relay.js: WireGuard setup improvements - router.js: Expanded proxy and vhost settings - sniffer.js: Enhanced capture configuration - wizard.js: Improved mode selection flow - RPCD backend enhancements (+176 lines) - Add deployment script for easier testing Claude settings: Update permissions for network-modes work Preparation for implementing features documented in CODEX-v0.3.5.md --- .claude/settings.local.json | 16 +- luci-app-network-modes/Makefile | 2 +- luci-app-network-modes/README.md | 9 +- .../resources/network-modes/api.js | 143 ++++++++++++++ .../resources/network-modes/helpers.js | 59 ++++++ .../view/network-modes/accesspoint.js | 62 ++++-- .../resources/view/network-modes/relay.js | 123 +++++++++++- .../resources/view/network-modes/router.js | 98 ++++++++-- .../resources/view/network-modes/sniffer.js | 29 ++- .../resources/view/network-modes/wizard.js | 16 +- .../root/usr/libexec/rpcd/luci.network-modes | 176 +++++++++++++++++- secubox-tools/deploy-network-modes.sh | 59 ++++++ 12 files changed, 740 insertions(+), 52 deletions(-) create mode 100644 luci-app-network-modes/htdocs/luci-static/resources/network-modes/helpers.js create mode 100755 secubox-tools/deploy-network-modes.sh diff --git a/.claude/settings.local.json b/.claude/settings.local.json index b848155..a4ac868 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -130,7 +130,21 @@ "Bash(./secubox-tools/add-pkg-file-modes.sh:*)", "Bash(luci-app-secubox/htdocs/luci-static/resources/view/secubox/dashboard.js )", "Bash(luci-app-secubox/htdocs/luci-static/resources/view/secubox/modules.js )", - "Bash(git checkout:*)" + "Bash(git checkout:*)", + "Bash(./scripts/setup-github-pages.sh:*)", + "Bash(mkdocs:*)", + "Bash(apt search:*)", + "Bash(source .venv/bin/activate)", + "Bash(pip install:*)", + "Bash(./scripts/setup-wiki.sh:*)", + "Bash(gh repo edit --help:*)", + "Bash(gh repo edit:*)", + "Bash(gh auth status:*)", + "Bash(git ls-remote:*)", + "Bash(for module in luci-app-ksm-manager luci-app-media-flow luci-app-netdata-dashboard luci-app-netifyd-dashboard luci-app-network-modes luci-app-secubox luci-app-system-hub luci-app-traffic-shaper luci-app-vhost-manager luci-app-wireguard-dashboard)", + "Bash(do echo \"=== $module ===\" find \"$module/htdocs/luci-static/resources/view\" -name \"*.js\")", + "Bash(gh run view:*)", + "Bash(/tmp/deploy-system-hub-overview-fix.sh)" ] } } diff --git a/luci-app-network-modes/Makefile b/luci-app-network-modes/Makefile index 52f230a..a3f4be6 100644 --- a/luci-app-network-modes/Makefile +++ b/luci-app-network-modes/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-network-modes -PKG_VERSION:=0.3.1 +PKG_VERSION:=0.3.5 PKG_RELEASE:=1 PKG_LICENSE:=Apache-2.0 diff --git a/luci-app-network-modes/README.md b/luci-app-network-modes/README.md index adea05a..17c721b 100644 --- a/luci-app-network-modes/README.md +++ b/luci-app-network-modes/README.md @@ -1,11 +1,11 @@ # LuCI Network Modes Dashboard -**Version:** 1.0.0 +**Version:** 0.3.5 **Last Updated:** 2025-12-28 **Status:** Active -![Version](https://img.shields.io/badge/version-1.0.0-orange) +![Version](https://img.shields.io/badge/version-0.3.5-orange) ![License](https://img.shields.io/badge/license-Apache--2.0-green) ![OpenWrt](https://img.shields.io/badge/OpenWrt-21.02+-blue) @@ -15,6 +15,11 @@ Configure your OpenWrt router for different network operation modes with a moder ## 🎯 Network Modes +### 🚀 What's New in v0.3.5 +- **WireGuard automation:** generate key pairs, deploy `wg0` interfaces, and push MTU/MSS/BBR optimizations directly from the Relay panel. +- **Optimization RPCs:** new backend methods expose MTU clamping, TCP BBR, and WireGuard deployment to both UI and automation agents. +- **UI action buttons:** Relay mode now includes one-click buttons for key generation, interface deployment, and optimization runs. + ### 🔍 Sniffer Bridge Mode (Inline / Passthrough) Transparent Ethernet bridge without IP address for in-line traffic analysis. All traffic passes through the device. diff --git a/luci-app-network-modes/htdocs/luci-static/resources/network-modes/api.js b/luci-app-network-modes/htdocs/luci-static/resources/network-modes/api.js index 048dbc3..1f58f06 100644 --- a/luci-app-network-modes/htdocs/luci-static/resources/network-modes/api.js +++ b/luci-app-network-modes/htdocs/luci-static/resources/network-modes/api.js @@ -34,6 +34,30 @@ var callSetMode = rpc.declare({ params: ['mode'] }); +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: { } +}); + var callGetInterfaces = rpc.declare({ object: 'luci.network-modes', method: 'get_interfaces', @@ -47,6 +71,71 @@ var callValidateConfig = rpc.declare({ expect: { valid: false, errors: [] } }); +var callSnifferConfig = rpc.declare({ + object: 'luci.network-modes', + method: 'sniffer_config', + expect: { } +}); + +var callApConfig = rpc.declare({ + object: 'luci.network-modes', + method: 'ap_config', + expect: { } +}); + +var callRelayConfig = rpc.declare({ + object: 'luci.network-modes', + method: 'relay_config', + expect: { } +}); + +var callRouterConfig = rpc.declare({ + object: 'luci.network-modes', + method: 'router_config', + expect: { } +}); + +var callUpdateSettings = rpc.declare({ + object: 'luci.network-modes', + method: 'update_settings' +}); + +var callAddVhost = rpc.declare({ + object: 'luci.network-modes', + method: 'add_vhost' +}); + +var callGenerateConfig = rpc.declare({ + object: 'luci.network-modes', + method: 'generate_config', + params: ['mode'], + expect: { } +}); + +var callGenerateWireguardKeys = rpc.declare({ + object: 'luci.network-modes', + method: 'generate_wireguard_keys', + expect: { } +}); + +var callApplyWireguardConfig = rpc.declare({ + object: 'luci.network-modes', + method: 'apply_wireguard_config', + expect: { } +}); + +var callApplyMtuClamping = rpc.declare({ + object: 'luci.network-modes', + method: 'apply_mtu_clamping', + expect: { } +}); + +var callEnableTcpBbr = rpc.declare({ + object: 'luci.network-modes', + method: 'enable_tcp_bbr', + expect: { } +}); + return baseclass.extend({ getStatus: callStatus, getCurrentMode: callGetCurrentMode, @@ -54,6 +143,9 @@ return baseclass.extend({ setMode: callSetMode, getInterfaces: callGetInterfaces, validateConfig: callValidateConfig, + previewChanges: callPreviewChanges, + confirmMode: callConfirmMode, + rollbackMode: callRollback, // Aggregate function for overview page getAllData: function() { @@ -77,6 +169,23 @@ return baseclass.extend({ }); }, + applyMode: function(targetMode) { + var chain = Promise.resolve(); + + if (targetMode) { + chain = callSetMode({ mode: targetMode }).then(function(result) { + if (!result || result.success === false) { + return Promise.reject(new Error((result && result.error) || 'Unable to prepare mode')); + } + return result; + }); + } + + return chain.then(function() { + return callApplyMode(); + }); + }, + // Get static information about a mode getModeInfo: function(mode) { var modeInfo = { @@ -160,5 +269,39 @@ return baseclass.extend({ var minutes = Math.floor((seconds % 3600) / 60); return days + 'd ' + hours + 'h ' + minutes + 'm'; + }, + + getSnifferConfig: callSnifferConfig, + getApConfig: callApConfig, + getRelayConfig: callRelayConfig, + getRouterConfig: callRouterConfig, + + updateSettings: function(mode, settings) { + var payload = Object.assign({}, settings || {}, { mode: mode }); + return callUpdateSettings(payload); + }, + + addVirtualHost: function(vhost) { + return callAddVhost(vhost); + }, + + generateConfig: function(mode) { + return callGenerateConfig({ mode: mode }); + }, + + generateWireguardKeys: function() { + return callGenerateWireguardKeys(); + }, + + applyWireguardConfig: function() { + return callApplyWireguardConfig(); + }, + + applyMtuClamping: function() { + return callApplyMtuClamping(); + }, + + enableTcpBbr: function() { + return callEnableTcpBbr(); } }); diff --git a/luci-app-network-modes/htdocs/luci-static/resources/network-modes/helpers.js b/luci-app-network-modes/htdocs/luci-static/resources/network-modes/helpers.js new file mode 100644 index 0000000..e8bfc25 --- /dev/null +++ b/luci-app-network-modes/htdocs/luci-static/resources/network-modes/helpers.js @@ -0,0 +1,59 @@ +'use strict'; +'require ui'; +'require network-modes.api as api'; + +function isToggleActive(node) { + return !!(node && node.classList.contains('active')); +} + +function persistSettings(mode, payload) { + ui.showModal(_('Saving settings...'), [ + E('p', { 'class': 'spinning' }, _('Applying configuration changes...')) + ]); + + return api.updateSettings(mode, payload).then(function(result) { + ui.hideModal(); + if (result && result.success) { + ui.addNotification(null, E('p', {}, result.message || _('Settings updated')), 'info'); + } else { + ui.addNotification(null, E('p', {}, (result && result.error) || _('Failed to update settings')), 'error'); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, err.message || err), 'error'); + }); +} + +function showGeneratedConfig(mode) { + ui.showModal(_('Generating configuration...'), [ + E('p', { 'class': 'spinning' }, _('Building configuration preview...')) + ]); + + return api.generateConfig(mode).then(function(result) { + ui.hideModal(); + + if (!result || !result.config) { + ui.addNotification(null, E('p', {}, _('No configuration data returned')), 'error'); + return; + } + + ui.showModal(_('Configuration Preview'), [ + E('pre', { 'class': 'nm-config-preview' }, result.config), + E('div', { 'class': 'right', 'style': 'margin-top: 16px;' }, [ + E('button', { + 'class': 'cbi-button cbi-button-positive', + 'click': ui.hideModal + }, _('Close')) + ]) + ]); + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, err.message || err), 'error'); + }); +} + +return { + isToggleActive: isToggleActive, + persistSettings: persistSettings, + showGeneratedConfig: showGeneratedConfig +}; diff --git a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/accesspoint.js b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/accesspoint.js index 9e9fb3d..6412d5e 100644 --- a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/accesspoint.js +++ b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/accesspoint.js @@ -3,6 +3,7 @@ 'require dom'; 'require ui'; 'require network-modes.api as api'; +'require network-modes.helpers as helpers'; return view.extend({ title: _('Access Point Mode'), @@ -210,26 +211,17 @@ return view.extend({ // Actions E('div', { 'class': 'nm-btn-group' }, [ - E('button', { 'class': 'nm-btn nm-btn-primary' }, [ + E('button', { 'class': 'nm-btn nm-btn-primary', 'data-action': 'ap-save', 'type': 'button' }, [ E('span', {}, '💾'), 'Save Settings' ]), - E('button', { 'class': 'nm-btn' }, [ - E('span', {}, '🔄'), - 'Restart WiFi' + E('button', { 'class': 'nm-btn', 'data-action': 'ap-config', 'type': 'button' }, [ + E('span', {}, '📝'), + 'Generate Config' ]) ]) ]); - // TX Power slider handler - var slider = view.querySelector('#wifi-txpower'); - var valueDisplay = view.querySelector('#txpower-value'); - if (slider && valueDisplay) { - slider.addEventListener('input', function() { - valueDisplay.textContent = this.value + ' dBm'; - }); - } - // Toggle handlers view.querySelectorAll('.nm-toggle-switch').forEach(function(toggle) { toggle.addEventListener('click', function() { @@ -241,10 +233,48 @@ return view.extend({ var cssLink = E('link', { 'rel': 'stylesheet', 'href': L.resource('network-modes/dashboard.css') }); document.head.appendChild(cssLink); + this.bindAccessPointActions(view); + return view; }, - handleSaveApply: null, - handleSave: null, - handleReset: null + bindAccessPointActions: function(container) { + var slider = container.querySelector('#wifi-txpower'); + var valueDisplay = container.querySelector('#txpower-value'); + if (slider && valueDisplay) { + slider.addEventListener('input', function() { + valueDisplay.textContent = this.value + ' dBm'; + }); + } + + var saveBtn = container.querySelector('[data-action="ap-save"]'); + var configBtn = container.querySelector('[data-action="ap-config"]'); + + if (saveBtn) + saveBtn.addEventListener('click', ui.createHandlerFn(this, 'saveAccessPointSettings', container)); + if (configBtn) + configBtn.addEventListener('click', ui.createHandlerFn(helpers, helpers.showGeneratedConfig, 'accesspoint')); + }, + + saveAccessPointSettings: function(container) { + var toggles = {}; + container.querySelectorAll('.nm-toggle-switch[data-setting]').forEach(function(toggle) { + var key = toggle.getAttribute('data-setting'); + toggles[key] = helpers.isToggleActive(toggle); + }); + + var payload = { + wifi_channel: container.querySelector('#wifi-channel') ? container.querySelector('#wifi-channel').value : 'auto', + wifi_htmode: container.querySelector('#wifi-htmode') ? container.querySelector('#wifi-htmode').value : 'VHT80', + wifi_txpower: container.querySelector('#wifi-txpower') ? parseInt(container.querySelector('#wifi-txpower').value, 10) : 20, + roaming_enabled: toggles.roaming_80211r ? 1 : 0, + band_steering: toggles.band_steering ? 1 : 0, + rrm_enabled: toggles.rrm_80211k ? 1 : 0, + wnm_enabled: toggles.wnm_80211v ? 1 : 0, + airtime_fairness: toggles.airtime_fairness ? 1 : 0, + beamforming: toggles.beamforming ? 1 : 0 + }; + + return helpers.persistSettings('accesspoint', payload); + } }); diff --git a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/relay.js b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/relay.js index 76da555..6c674c6 100644 --- a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/relay.js +++ b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/relay.js @@ -3,6 +3,7 @@ 'require dom'; 'require ui'; 'require network-modes.api as api'; +'require network-modes.helpers as helpers'; return view.extend({ title: _('Relay Mode'), @@ -45,6 +46,10 @@ return view.extend({ ]), E('div', { 'class': 'nm-card-badge' }, config.relayd_available ? 'Relayd Available' : 'Relayd Not Installed') ]), + E('div', { 'class': 'nm-btn-group', 'style': 'margin-top: 16px' }, [ + E('button', { 'class': 'nm-btn', 'data-action': 'relay-generate-keys', 'type': 'button' }, '🔑 Generate Keys'), + E('button', { 'class': 'nm-btn', 'data-action': 'relay-apply-wireguard', 'type': 'button' }, '🚀 Deploy Interface') + ]) E('div', { 'class': 'nm-card-body' }, [ E('div', { 'class': 'nm-form-group' }, [ E('label', { 'class': 'nm-form-label' }, 'Relay Interface (Upstream)'), @@ -79,6 +84,9 @@ return view.extend({ ]), E('div', { 'class': 'nm-card-badge' }, (config.wg_interfaces || []).length + ' tunnels') ]), + E('div', { 'class': 'nm-btn-group', 'style': 'margin-top: 12px' }, [ + E('button', { 'class': 'nm-btn', 'data-action': 'relay-apply-optimizations', 'type': 'button' }, '⚙️ Apply Optimizations') + ]) E('div', { 'class': 'nm-card-body' }, [ E('div', { 'class': 'nm-toggle' }, [ E('div', { 'class': 'nm-toggle-info' }, [ @@ -191,13 +199,13 @@ return view.extend({ // Actions E('div', { 'class': 'nm-btn-group' }, [ - E('button', { 'class': 'nm-btn nm-btn-primary' }, [ + E('button', { 'class': 'nm-btn nm-btn-primary', 'data-action': 'relay-save', 'type': 'button' }, [ E('span', {}, '💾'), 'Save Settings' ]), - E('button', { 'class': 'nm-btn' }, [ - E('span', {}, '🔄'), - 'Apply & Restart' + E('button', { 'class': 'nm-btn', 'data-action': 'relay-config', 'type': 'button' }, [ + E('span', {}, '📝'), + 'Generate Config' ]) ]) ]); @@ -213,10 +221,111 @@ return view.extend({ var cssLink = E('link', { 'rel': 'stylesheet', 'href': L.resource('network-modes/dashboard.css') }); document.head.appendChild(cssLink); + this.bindRelayActions(view); + return view; }, - handleSaveApply: null, - handleSave: null, - handleReset: null + bindRelayActions: function(container) { + var saveBtn = container.querySelector('[data-action="relay-save"]'); + var configBtn = container.querySelector('[data-action="relay-config"]'); + + if (saveBtn) + saveBtn.addEventListener('click', ui.createHandlerFn(this, 'saveRelaySettings', container)); + if (configBtn) + configBtn.addEventListener('click', ui.createHandlerFn(helpers, helpers.showGeneratedConfig, 'relay')); + var generateBtn = container.querySelector('[data-action="relay-generate-keys"]'); + var deployBtn = container.querySelector('[data-action="relay-apply-wireguard"]'); + var optimizeBtn = container.querySelector('[data-action="relay-apply-optimizations"]'); + + if (generateBtn) + generateBtn.addEventListener('click', ui.createHandlerFn(this, 'generateWireguardKeys')); + if (deployBtn) + deployBtn.addEventListener('click', ui.createHandlerFn(this, 'deployWireguardInterface')); + if (optimizeBtn) + optimizeBtn.addEventListener('click', ui.createHandlerFn(this, 'applyOptimizations')); + }, + + saveRelaySettings: function(container) { + var toggles = {}; + container.querySelectorAll('.nm-toggle-switch[data-opt]').forEach(function(toggle) { + var key = toggle.getAttribute('data-opt'); + toggles[key] = helpers.isToggleActive(toggle); + }); + + var mtuValue = container.querySelector('#wg-mtu') ? parseInt(container.querySelector('#wg-mtu').value, 10) : 1420; + var conntrackValue = container.querySelector('#conntrack-max') ? parseInt(container.querySelector('#conntrack-max').value, 10) : 16384; + + var payload = { + wireguard_enabled: helpers.isToggleActive(container.querySelector('#toggle-wg')) ? 1 : 0, + wireguard_interface: container.querySelector('#wg-interface') ? container.querySelector('#wg-interface').value : '', + relay_interface: container.querySelector('#relay-interface') ? container.querySelector('#relay-interface').value : '', + lan_interface: container.querySelector('#lan-interface') ? container.querySelector('#lan-interface').value : '', + wireguard_mtu: isNaN(mtuValue) ? 1420 : mtuValue, + mtu_optimization: toggles.mtu_optimization ? 1 : 0, + mss_clamping: toggles.mss_clamping ? 1 : 0, + tcp_optimization: toggles.tcp_optimization ? 1 : 0, + conntrack_max: isNaN(conntrackValue) ? 16384 : conntrackValue + }; + + return helpers.persistSettings('relay', payload); + }, + + generateWireguardKeys: function() { + ui.showModal(_('Generating WireGuard keys...'), [ + E('p', { 'class': 'spinning' }, _('wg genkey / wg pubkey')) + ]); + + return api.generateWireguardKeys().then(function(result) { + ui.hideModal(); + if (result && result.success) { + ui.addNotification(null, E('p', {}, _('New keys generated')), 'info'); + } else { + ui.addNotification(null, E('p', {}, (result && result.error) || _('Key generation failed')), 'error'); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, err.message || err), 'error'); + }); + }, + + deployWireguardInterface: function() { + ui.showModal(_('Deploying WireGuard interface...'), [ + E('p', { 'class': 'spinning' }, _('Writing /etc/config/network and reloading interfaces')) + ]); + + return api.applyWireguardConfig().then(function(result) { + ui.hideModal(); + if (result && result.success) { + ui.addNotification(null, E('p', {}, _('WireGuard interface deployed')), 'info'); + } else { + ui.addNotification(null, E('p', {}, (result && result.error) || _('Deployment failed')), 'error'); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, err.message || err), 'error'); + }); + }, + + applyOptimizations: function() { + ui.showModal(_('Applying optimizations...'), [ + E('p', { 'class': 'spinning' }, _('Configuring firewall MSS clamping and TCP BBR')) + ]); + + return Promise.all([ + api.applyMtuClamping(), + api.enableTcpBbr() + ]).then(function(responses) { + ui.hideModal(); + var errors = responses.filter(function(r) { return r && r.success === 0; }); + if (errors.length > 0) { + ui.addNotification(null, E('p', {}, (errors[0].error || _('Optimization failed'))), 'error'); + } else { + ui.addNotification(null, E('p', {}, _('Optimizations applied')), 'info'); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, err.message || err), 'error'); + }); + } }); diff --git a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/router.js b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/router.js index ad8a347..acc1d87 100644 --- a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/router.js +++ b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/router.js @@ -3,6 +3,7 @@ 'require dom'; 'require ui'; 'require network-modes.api as api'; +'require network-modes.helpers as helpers'; return view.extend({ title: _('Router Mode'), @@ -300,14 +301,18 @@ return view.extend({ // Add Virtual Host E('div', { 'style': 'margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--nm-border)' }, [ E('h4', { 'style': 'margin: 0 0 12px 0; font-size: 14px' }, 'Add Virtual Host'), - E('div', { 'style': 'display: grid; grid-template-columns: 2fr 2fr 1fr auto; gap: 12px; align-items: end' }, [ + E('div', { 'style': 'display: grid; grid-template-columns: 2fr 2fr 1fr 1fr auto; gap: 12px; align-items: end' }, [ E('div', { 'class': 'nm-form-group', 'style': 'margin: 0' }, [ E('label', { 'class': 'nm-form-label' }, 'Domain'), E('input', { 'class': 'nm-input', 'type': 'text', 'placeholder': 'example.com', 'id': 'new-domain' }) ]), E('div', { 'class': 'nm-form-group', 'style': 'margin: 0' }, [ E('label', { 'class': 'nm-form-label' }, 'Backend'), - E('input', { 'class': 'nm-input', 'type': 'text', 'placeholder': '127.0.0.1:8080', 'id': 'new-backend' }) + E('input', { 'class': 'nm-input', 'type': 'text', 'placeholder': '127.0.0.1', 'id': 'new-backend' }) + ]), + E('div', { 'class': 'nm-form-group', 'style': 'margin: 0' }, [ + E('label', { 'class': 'nm-form-label' }, 'Port'), + E('input', { 'class': 'nm-input', 'type': 'number', 'min': '1', 'max': '65535', 'value': '443', 'id': 'new-port' }) ]), E('div', { 'class': 'nm-form-group', 'style': 'margin: 0' }, [ E('label', { 'class': 'nm-form-label' }, 'SSL'), @@ -316,7 +321,7 @@ return view.extend({ E('option', { 'value': '0' }, 'No') ]) ]), - E('button', { 'class': 'nm-btn nm-btn-primary', 'style': 'height: 46px' }, '➕ Add') + E('button', { 'class': 'nm-btn nm-btn-primary', 'style': 'height: 46px', 'type': 'button', 'data-action': 'router-add-vhost' }, '➕ Add') ]) ]) ]) @@ -324,15 +329,15 @@ return view.extend({ // Actions E('div', { 'class': 'nm-btn-group' }, [ - E('button', { 'class': 'nm-btn nm-btn-primary' }, [ + E('button', { 'class': 'nm-btn nm-btn-primary', 'data-action': 'router-save', 'type': 'button' }, [ E('span', {}, '💾'), 'Save Settings' ]), - E('button', { 'class': 'nm-btn' }, [ - E('span', {}, '🔄'), - 'Apply & Restart' + E('button', { 'class': 'nm-btn', 'data-action': 'router-wizard', 'type': 'button' }, [ + E('span', {}, '🧭'), + 'Open Mode Wizard' ]), - E('button', { 'class': 'nm-btn' }, [ + E('button', { 'class': 'nm-btn', 'data-action': 'router-config', 'type': 'button' }, [ E('span', {}, '📝'), 'Generate Config' ]) @@ -350,10 +355,81 @@ return view.extend({ var cssLink = E('link', { 'rel': 'stylesheet', 'href': L.resource('network-modes/dashboard.css') }); document.head.appendChild(cssLink); + this.bindRouterActions(view); + return view; }, - handleSaveApply: null, - handleSave: null, - handleReset: null + bindRouterActions: function(container) { + var saveBtn = container.querySelector('[data-action="router-save"]'); + var wizardBtn = container.querySelector('[data-action="router-wizard"]'); + var configBtn = container.querySelector('[data-action="router-config"]'); + var addVhostBtn = container.querySelector('[data-action="router-add-vhost"]'); + + if (saveBtn) + saveBtn.addEventListener('click', ui.createHandlerFn(this, 'saveRouterSettings', container)); + if (wizardBtn) + wizardBtn.addEventListener('click', ui.createHandlerFn(this, 'openWizard')); + if (configBtn) + configBtn.addEventListener('click', ui.createHandlerFn(helpers, helpers.showGeneratedConfig, 'router')); + if (addVhostBtn) + addVhostBtn.addEventListener('click', ui.createHandlerFn(this, 'addVirtualHost', container)); + }, + + saveRouterSettings: function(container) { + var payload = { + wan_interface: container.querySelector('#wan-interface') ? container.querySelector('#wan-interface').value : 'eth1', + wan_protocol: container.querySelector('#wan-protocol') ? container.querySelector('#wan-protocol').value : 'dhcp', + nat_enabled: helpers.isToggleActive(container.querySelector('#toggle-nat')) ? 1 : 0, + firewall_enabled: helpers.isToggleActive(container.querySelector('#toggle-firewall')) ? 1 : 0, + proxy_enabled: helpers.isToggleActive(container.querySelector('#toggle-proxy')) ? 1 : 0, + proxy_type: container.querySelector('#proxy-type') ? container.querySelector('#proxy-type').value : 'squid', + proxy_port: container.querySelector('#proxy-port') ? parseInt(container.querySelector('#proxy-port').value, 10) || 3128 : 3128, + transparent_proxy: helpers.isToggleActive(container.querySelector('#toggle-transparent')) ? 1 : 0, + dns_over_https: helpers.isToggleActive(container.querySelector('#toggle-doh')) ? 1 : 0, + https_frontend: helpers.isToggleActive(container.querySelector('#toggle-frontend')) ? 1 : 0, + frontend_type: container.querySelector('#frontend-type') ? container.querySelector('#frontend-type').value : 'nginx', + letsencrypt: helpers.isToggleActive(container.querySelector('#toggle-letsencrypt')) ? 1 : 0 + }; + + return helpers.persistSettings('router', payload); + }, + + openWizard: function() { + window.location.hash = '#admin/secubox/network/network-modes/wizard'; + }, + + addVirtualHost: function(container) { + var domain = container.querySelector('#new-domain').value.trim(); + var backend = container.querySelector('#new-backend').value.trim(); + var portValue = parseInt(container.querySelector('#new-port').value, 10); + var sslValue = container.querySelector('#new-ssl').value === '1' ? 1 : 0; + + if (!domain || !backend) { + ui.addNotification(null, E('p', {}, _('Domain and backend are required')), 'error'); + return; + } + + ui.showModal(_('Adding virtual host...'), [ + E('p', { 'class': 'spinning' }, _('Saving virtual host entry')) + ]); + + return api.addVirtualHost({ + domain: domain, + backend: backend, + port: isNaN(portValue) ? 80 : portValue, + ssl: sslValue + }).then(function(result) { + ui.hideModal(); + if (result && result.success) { + ui.addNotification(null, E('p', {}, result.message || _('Virtual host added')), 'info'); + window.location.reload(); + } else { + ui.addNotification(null, E('p', {}, (result && result.error) || _('Failed to add virtual host')), 'error'); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, err.message || err), 'error'); + }); + } }); diff --git a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/sniffer.js b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/sniffer.js index 620d2e0..d3eb531 100644 --- a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/sniffer.js +++ b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/sniffer.js @@ -3,6 +3,7 @@ 'require dom'; 'require ui'; 'require network-modes.api as api'; +'require network-modes.helpers as helpers'; return view.extend({ title: _('Sniffer Mode'), @@ -130,11 +131,11 @@ return view.extend({ // Actions E('div', { 'class': 'nm-btn-group' }, [ - E('button', { 'class': 'nm-btn nm-btn-primary' }, [ + E('button', { 'class': 'nm-btn nm-btn-primary', 'data-action': 'sniffer-save', 'type': 'button' }, [ E('span', {}, '💾'), 'Save Settings' ]), - E('button', { 'class': 'nm-btn' }, [ + E('button', { 'class': 'nm-btn', 'data-action': 'sniffer-config', 'type': 'button' }, [ E('span', {}, '🔄'), 'Apply & Restart' ]) @@ -152,10 +153,28 @@ return view.extend({ var cssLink = E('link', { 'rel': 'stylesheet', 'href': L.resource('network-modes/dashboard.css') }); document.head.appendChild(cssLink); + this.bindSnifferActions(view); + return view; }, - handleSaveApply: null, - handleSave: null, - handleReset: null + bindSnifferActions: function(container) { + var saveBtn = container.querySelector('[data-action="sniffer-save"]'); + var applyBtn = container.querySelector('[data-action="sniffer-config"]'); + + if (saveBtn) + saveBtn.addEventListener('click', ui.createHandlerFn(this, 'saveSnifferSettings', container)); + if (applyBtn) + applyBtn.addEventListener('click', ui.createHandlerFn(helpers, helpers.showGeneratedConfig, 'sniffer')); + }, + + saveSnifferSettings: function(container) { + var payload = { + bridge_interface: container.querySelector('#bridge-interface') ? container.querySelector('#bridge-interface').value : '', + netifyd_enabled: helpers.isToggleActive(container.querySelector('#toggle-netifyd')) ? 1 : 0, + promiscuous: helpers.isToggleActive(container.querySelector('#toggle-promisc')) ? 1 : 0 + }; + + return helpers.persistSettings('sniffer', payload); + } }); diff --git a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/wizard.js b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/wizard.js index eb6bb76..3e65f0b 100644 --- a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/wizard.js +++ b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/wizard.js @@ -6,44 +6,44 @@ 'require poll'; var callGetAvailableModes = rpc.declare({ - object: 'luci.network_modes', + object: 'luci.network-modes', method: 'get_available_modes', expect: { modes: [] } }); var callGetCurrentMode = rpc.declare({ - object: 'luci.network_modes', + object: 'luci.network-modes', method: 'get_current_mode', expect: { } }); var callSetMode = rpc.declare({ - object: 'luci.network_modes', + object: 'luci.network-modes', method: 'set_mode', params: ['mode'], expect: { } }); var callPreviewChanges = rpc.declare({ - object: 'luci.network_modes', + object: 'luci.network-modes', method: 'preview_changes', expect: { } }); var callApplyMode = rpc.declare({ - object: 'luci.network_modes', + object: 'luci.network-modes', method: 'apply_mode', expect: { } }); var callConfirmMode = rpc.declare({ - object: 'luci.network_modes', + object: 'luci.network-modes', method: 'confirm_mode', expect: { } }); var callRollback = rpc.declare({ - object: 'luci.network_modes', + object: 'luci.network-modes', method: 'rollback', expect: { } }); @@ -228,7 +228,7 @@ return view.extend({ E('p', { 'class': 'spinning' }, _('Préparation du changement de mode...')) ]); - return callSetMode(mode.id).then(L.bind(function(result) { + return callSetMode({ mode: mode.id }).then(L.bind(function(result) { if (!result.success) { ui.hideModal(); ui.addNotification(null, E('p', result.error || _('Erreur')), 'error'); diff --git a/luci-app-network-modes/root/usr/libexec/rpcd/luci.network-modes b/luci-app-network-modes/root/usr/libexec/rpcd/luci.network-modes index 3b61cfd..ff605e1 100755 --- a/luci-app-network-modes/root/usr/libexec/rpcd/luci.network-modes +++ b/luci-app-network-modes/root/usr/libexec/rpcd/luci.network-modes @@ -455,6 +455,10 @@ apply_mode() { uci set network.stabridge=interface uci set network.stabridge.proto='relay' uci set network.stabridge.network='lan wwan' + + apply_wireguard_config + apply_mtu_clamping + enable_tcp_bbr ;; bridge) @@ -542,10 +546,12 @@ update_settings() { case "$mode" in sniffer) json_get_var bridge_interface bridge_interface + json_get_var capture_interface capture_interface json_get_var netifyd_enabled netifyd_enabled json_get_var promiscuous promiscuous [ -n "$bridge_interface" ] && uci set network-modes.sniffer.bridge_interface="$bridge_interface" + [ -n "$capture_interface" ] && uci set network-modes.sniffer.capture_interface="$capture_interface" [ -n "$netifyd_enabled" ] && uci set network-modes.sniffer.netifyd_enabled="$netifyd_enabled" [ -n "$promiscuous" ] && uci set network-modes.sniffer.promiscuous="$promiscuous" ;; @@ -555,40 +561,68 @@ update_settings() { json_get_var wifi_txpower wifi_txpower json_get_var roaming_enabled roaming_enabled json_get_var band_steering band_steering + json_get_var rrm_enabled rrm_enabled + json_get_var wnm_enabled wnm_enabled + json_get_var airtime_fairness airtime_fairness + json_get_var beamforming beamforming [ -n "$wifi_channel" ] && uci set network-modes.accesspoint.wifi_channel="$wifi_channel" [ -n "$wifi_htmode" ] && uci set network-modes.accesspoint.wifi_htmode="$wifi_htmode" [ -n "$wifi_txpower" ] && uci set network-modes.accesspoint.wifi_txpower="$wifi_txpower" [ -n "$roaming_enabled" ] && uci set network-modes.accesspoint.roaming_enabled="$roaming_enabled" [ -n "$band_steering" ] && uci set network-modes.accesspoint.band_steering="$band_steering" + [ -n "$rrm_enabled" ] && uci set network-modes.accesspoint.rrm_enabled="$rrm_enabled" + [ -n "$wnm_enabled" ] && uci set network-modes.accesspoint.wnm_enabled="$wnm_enabled" + [ -n "$airtime_fairness" ] && uci set network-modes.accesspoint.airtime_fairness="$airtime_fairness" + [ -n "$beamforming" ] && uci set network-modes.accesspoint.beamforming="$beamforming" ;; relay) json_get_var wireguard_enabled wireguard_enabled json_get_var wireguard_interface wireguard_interface + json_get_var wireguard_mtu wireguard_mtu json_get_var mtu_optimization mtu_optimization json_get_var mss_clamping mss_clamping + json_get_var relay_interface relay_interface + json_get_var lan_interface lan_interface + json_get_var tcp_optimization tcp_optimization + json_get_var conntrack_max conntrack_max [ -n "$wireguard_enabled" ] && uci set network-modes.relay.wireguard_enabled="$wireguard_enabled" [ -n "$wireguard_interface" ] && uci set network-modes.relay.wireguard_interface="$wireguard_interface" + [ -n "$wireguard_mtu" ] && uci set network-modes.relay.wireguard_mtu="$wireguard_mtu" + [ -n "$relay_interface" ] && uci set network-modes.relay.relay_interface="$relay_interface" + [ -n "$lan_interface" ] && uci set network-modes.relay.lan_interface="$lan_interface" [ -n "$mtu_optimization" ] && uci set network-modes.relay.mtu_optimization="$mtu_optimization" [ -n "$mss_clamping" ] && uci set network-modes.relay.mss_clamping="$mss_clamping" + [ -n "$tcp_optimization" ] && uci set network-modes.relay.tcp_optimization="$tcp_optimization" + [ -n "$conntrack_max" ] && uci set network-modes.relay.conntrack_max="$conntrack_max" ;; router) + json_get_var wan_interface wan_interface json_get_var wan_protocol wan_protocol json_get_var nat_enabled nat_enabled json_get_var firewall_enabled firewall_enabled json_get_var proxy_enabled proxy_enabled json_get_var proxy_type proxy_type + json_get_var proxy_port proxy_port json_get_var https_frontend https_frontend json_get_var frontend_type frontend_type + json_get_var transparent_proxy transparent_proxy + json_get_var dns_over_https dns_over_https + json_get_var letsencrypt letsencrypt + [ -n "$wan_interface" ] && uci set network-modes.router.wan_interface="$wan_interface" [ -n "$wan_protocol" ] && uci set network-modes.router.wan_protocol="$wan_protocol" [ -n "$nat_enabled" ] && uci set network-modes.router.nat_enabled="$nat_enabled" [ -n "$firewall_enabled" ] && uci set network-modes.router.firewall_enabled="$firewall_enabled" [ -n "$proxy_enabled" ] && uci set network-modes.router.proxy_enabled="$proxy_enabled" [ -n "$proxy_type" ] && uci set network-modes.router.proxy_type="$proxy_type" + [ -n "$proxy_port" ] && uci set network-modes.router.proxy_port="$proxy_port" [ -n "$https_frontend" ] && uci set network-modes.router.https_frontend="$https_frontend" [ -n "$frontend_type" ] && uci set network-modes.router.frontend_type="$frontend_type" + [ -n "$transparent_proxy" ] && uci set network-modes.router.proxy_transparent="$transparent_proxy" + [ -n "$dns_over_https" ] && uci set network-modes.router.dns_over_https="$dns_over_https" + [ -n "$letsencrypt" ] && uci set network-modes.router.letsencrypt="$letsencrypt" ;; *) json_add_boolean "success" 0 @@ -605,6 +639,122 @@ update_settings() { json_dump } +# Generate WireGuard key pair and store in UCI +generate_wireguard_keys() { + json_init + + if ! command -v wg >/dev/null 2>&1; then + json_add_boolean "success" 0 + json_add_string "error" "wireguard-tools not installed" + json_dump + return + fi + + local privkey="$(wg genkey 2>/dev/null)" + if [ -z "$privkey" ]; then + json_add_boolean "success" 0 + json_add_string "error" "failed to generate private key" + json_dump + return + fi + + local pubkey + pubkey="$(printf '%s' "$privkey" | wg pubkey 2>/dev/null)" + + if [ -z "$pubkey" ]; then + json_add_boolean "success" 0 + json_add_string "error" "failed to derive public key" + json_dump + return + fi + + uci set network-modes.relay.wg_private_key="$privkey" + uci set network-modes.relay.wg_public_key="$pubkey" + uci commit network-modes + + json_add_boolean "success" 1 + json_add_string "private_key" "$privkey" + json_add_string "public_key" "$pubkey" + json_dump +} + +# Deploy WireGuard interface/peer config to /etc/config/network +apply_wireguard_config() { + local wg_enabled=$(uci -q get network-modes.relay.wireguard_enabled || echo "0") + [ "$wg_enabled" = "1" ] || return 0 + + local privkey=$(uci -q get network-modes.relay.wg_private_key) + local peer_pubkey=$(uci -q get network-modes.relay.wg_peer_pubkey) + local peer_endpoint=$(uci -q get network-modes.relay.wg_peer_endpoint) + local wg_port=$(uci -q get network-modes.relay.wg_port || echo "51820") + local wg_ip=$(uci -q get network-modes.relay.wg_ip || echo "10.200.200.2/24") + + [ -n "$privkey" ] || return 1 + [ -n "$peer_pubkey" ] || return 1 + [ -n "$peer_endpoint" ] || return 1 + + uci -q delete network.wg0 + uci -q delete network.wg0_peer + + uci set network.wg0=interface + uci set network.wg0.proto='wireguard' + uci set network.wg0.private_key="$privkey" + uci set network.wg0.listen_port="$wg_port" + uci add_list network.wg0.addresses="$wg_ip" + + uci set network.wg0_peer=wireguard_wg0 + uci set network.wg0_peer.public_key="$peer_pubkey" + uci set network.wg0_peer.endpoint_host="$(echo "$peer_endpoint" | cut -d: -f1)" + uci set network.wg0_peer.endpoint_port="$(echo "$peer_endpoint" | cut -d: -f2-)" + uci set network.wg0_peer.persistent_keepalive='25' + uci add_list network.wg0_peer.allowed_ips='0.0.0.0/0' + + uci commit network + /etc/init.d/network reload >/dev/null 2>&1 +} + +# Configure firewall MSS clamping based on configured MTU +apply_mtu_clamping() { + local mtu_optimization=$(uci -q get network-modes.relay.mtu_optimization || echo "0") + [ "$mtu_optimization" = "1" ] || return 0 + + local wg_mtu=$(uci -q get network-modes.relay.wg_mtu || echo "1420") + local mss_value=$((wg_mtu - 40)) + if [ "$mss_value" -lt 500 ]; then + mss_value=500 + fi + + uci -q delete firewall.mss_clamping + uci set firewall.mss_clamping=rule + uci set firewall.mss_clamping.name='WireGuard MSS Clamping' + uci set firewall.mss_clamping.src='lan' + uci set firewall.mss_clamping.dest='wan' + uci set firewall.mss_clamping.proto='tcp' + uci set firewall.mss_clamping.tcp_flags='SYN' + uci set firewall.mss_clamping.target='TCPMSS' + uci set firewall.mss_clamping.set_mss="$mss_value" + uci commit firewall + /etc/init.d/firewall reload >/dev/null 2>&1 +} + +# Enable TCP BBR congestion control if requested +enable_tcp_bbr() { + local tcp_optimize=$(uci -q get network-modes.relay.tcp_optimization || echo "0") + [ "$tcp_optimize" = "1" ] || return 0 + + if ! modprobe tcp_bbr 2>/dev/null; then + logger -t network-modes "tcp_bbr module unavailable" + return 1 + fi + + cat > /etc/sysctl.d/90-tcp-bbr.conf </dev/null 2>&1 +} + # Add virtual host add_vhost() { read input @@ -1100,7 +1250,7 @@ rollback() { # Main dispatcher case "$1" in list) - 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"}}' + 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"},"generate_wireguard_keys":{},"apply_wireguard_config":{},"apply_mtu_clamping":{},"enable_tcp_bbr":{},"add_vhost":{"domain":"str","backend":"str","port":"int","ssl":"bool"},"generate_config":{"mode":"str"}}' ;; call) case "$2" in @@ -1146,6 +1296,30 @@ case "$1" in update_settings) update_settings ;; + generate_wireguard_keys) + generate_wireguard_keys + ;; + apply_wireguard_config) + if apply_wireguard_config; then + json_init; json_add_boolean "success" 1; json_dump + else + json_init; json_add_boolean "success" 0; json_add_string "error" "WireGuard deployment failed"; json_dump + fi + ;; + apply_mtu_clamping) + if apply_mtu_clamping; then + json_init; json_add_boolean "success" 1; json_dump + else + json_init; json_add_boolean "success" 0; json_add_string "error" "Unable to update firewall rule"; json_dump + fi + ;; + enable_tcp_bbr) + if enable_tcp_bbr; then + json_init; json_add_boolean "success" 1; json_dump + else + json_init; json_add_boolean "success" 0; json_add_string "error" "TCP BBR not applied"; json_dump + fi + ;; add_vhost) add_vhost ;; diff --git a/secubox-tools/deploy-network-modes.sh b/secubox-tools/deploy-network-modes.sh new file mode 100755 index 0000000..8652c0f --- /dev/null +++ b/secubox-tools/deploy-network-modes.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Deploy luci-app-network-modes to an OpenWrt router. +# Usage: ./secubox-tools/deploy-network-modes.sh [root@192.168.1.1] [package.ipk] + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +ROUTER_HOST="${1:-root@192.168.1.1}" +PACKAGE_PATH="${2:-}" + +if [[ -z "$PACKAGE_PATH" ]]; then + echo "[1/4] Building luci-app-network-modes…" >&2 + (cd "$REPO_ROOT" && ./secubox-tools/local-build.sh build luci-app-network-modes) + + echo "[2/4] Locating IPK artifact…" >&2 + mapfile -t PKGS < <(cd "$REPO_ROOT" && find bin -type f -name 'luci-app-network-modes_*_all.ipk' -print 2>/dev/null | sort) + if [[ "${#PKGS[@]}" -eq 0 ]]; then + echo "ERROR: No luci-app-network-modes IPK found under bin/. Build step may have failed." >&2 + exit 1 + fi + PACKAGE_PATH="${PKGS[-1]}" +fi + +if [[ ! -f "$PACKAGE_PATH" ]]; then + echo "ERROR: Package file not found: $PACKAGE_PATH" >&2 + exit 1 +fi + +PACKAGE_PATH="$(cd "$(dirname "$PACKAGE_PATH")" && pwd)/$(basename "$PACKAGE_PATH")" +PKG_NAME="$(basename "$PACKAGE_PATH")" + +echo "[3/4] Uploading $PKG_NAME to $ROUTER_HOST:/tmp/" >&2 +scp "$PACKAGE_PATH" "${ROUTER_HOST}:/tmp/$PKG_NAME" + +echo "[4/4] Installing on router and restarting services…" >&2 +ssh "$ROUTER_HOST" "sh -s" </dev/null 2>&1; then + echo "[router] Detected apk – ensuring package database…" >&2 + apk add --allow-untrusted "\$PKG" +else + echo "[router] Using opkg…" >&2 + opkg remove luci-app-network-modes --force-depends >/dev/null 2>&1 || true + opkg install "\$PKG" +fi + +chmod 755 /usr/libexec/rpcd/luci.network-modes || true +chmod 644 /www/luci-static/resources/network-modes/* 2>/dev/null || true +rm -f /tmp/luci-indexcache /tmp/luci-modulecache/* 2>/dev/null || true +/etc/init.d/rpcd restart +/etc/init.d/uhttpd restart + +echo "[router] Deployment complete." +EOF + +echo "Deployment completed successfully."