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
This commit is contained in:
CyberMind-FR 2025-12-28 14:25:50 +01:00
parent 94bc005ec0
commit 562ac55fe1
12 changed files with 740 additions and 52 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <<EOF
net.core.default_qdisc=fq
net.ipv4.tcp_congestion_control=bbr
EOF
sysctl -p /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
;;

View File

@ -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" <<EOF
set -e
PKG="/tmp/$PKG_NAME"
if command -v apk >/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."