This release adds dual menu access for Network Modes (both SecuBox and LuCI Network menus) and significantly expands RPCD permissions for all mode configuration operations. ## Network Modes - Dual Menu Access (2 files) - Added Network Modes to standard LuCI Network menu (admin/network/modes) - Maintains existing SecuBox menu location (admin/secubox/network/modes) - Users can now access Network Modes from both locations - Menu order: 60 in Network menu, 10 in SecuBox Network category ## Network Modes - Enhanced Permissions (1 file) Added 13+ new RPCD methods to ACL for complete mode management: Read permissions: - preview_changes - sniffer_config, ap_config, relay_config, router_config - travel_config, doublenat_config, multiwan_config, vpnrelay_config - travel_scan_networks Write permissions: - apply_mode, confirm_mode, rollback - update_settings - generate_wireguard_keys, apply_wireguard_config - apply_mtu_clamping, enable_tcp_bbr - add_vhost, generate_config ## Network Modes - View Updates (11 files) Updated all mode views for consistency: - helpers.js: 28 lines refactored - overview.js: Enhanced view structure - All mode views: wizard, router, multiwan, doublenat, accesspoint, relay, vpnrelay, travel, sniffer ## Theme Enhancements (1 file) - theme.js: 89 lines added - Enhanced theme initialization and configuration - Improved component styling support ## SecuBox Dashboard (2 files) - Updated dashboard.js and modules.js - Improved view rendering and integration ## System Hub (3 files) - Enhanced logs.js, overview.js, services.js - Better view consistency and functionality Summary: - 19 files changed (+282, -36) - Dual menu access for Network Modes - 13+ new RPCD permission methods - All network mode views updated - Theme significantly enhanced 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
288 lines
12 KiB
JavaScript
288 lines
12 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require dom';
|
|
'require ui';
|
|
'require network-modes.api as api';
|
|
'require network-modes.helpers as helpers';
|
|
'require secubox-theme/theme as Theme';
|
|
|
|
var nmLang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
|
|
(document.documentElement && document.documentElement.getAttribute('lang')) ||
|
|
(navigator.language ? navigator.language.split('-')[0] : 'en');
|
|
Theme.init({ language: nmLang });
|
|
|
|
function buildOptToggle(key, icon, label, desc, enabled) {
|
|
return E('div', { 'class': 'nm-toggle' }, [
|
|
E('div', { 'class': 'nm-toggle-info' }, [
|
|
E('span', { 'class': 'nm-toggle-icon' }, icon),
|
|
E('div', {}, [
|
|
E('div', { 'class': 'nm-toggle-label' }, label),
|
|
E('div', { 'class': 'nm-toggle-desc' }, desc)
|
|
])
|
|
]),
|
|
E('div', {
|
|
'class': 'nm-toggle-switch' + (enabled ? ' active' : ''),
|
|
'data-opt': key
|
|
})
|
|
]);
|
|
}
|
|
|
|
return view.extend({
|
|
title: _('Relay Mode'),
|
|
|
|
load: function() {
|
|
return api.getRelayConfig();
|
|
},
|
|
|
|
render: function(data) {
|
|
var config = data || {};
|
|
var wgConfig = config.wireguard || {};
|
|
var optConfig = config.optimizations || {};
|
|
|
|
var hero = helpers.createHero({
|
|
icon: '🔁',
|
|
title: _('Relay Mode'),
|
|
subtitle: _('WiFi relay/bridge with optional WireGuard backhaul, MTU tuning, and TCP acceleration for remote sites.'),
|
|
gradient: 'linear-gradient(135deg,#10b981,#34d399)',
|
|
actions: [
|
|
E('button', { 'class': 'nm-btn', 'type': 'button', 'data-action': 'relay-generate-keys' }, ['🔑 ', _('Generate Keys')]),
|
|
E('button', { 'class': 'nm-btn', 'type': 'button', 'data-action': 'relay-apply-wireguard' }, ['🚀 ', _('Deploy Tunnel')]),
|
|
E('button', { 'class': 'nm-btn', 'type': 'button', 'data-action': 'relay-apply-optimizations' }, ['⚙️ ', _('Apply Optimizations')]),
|
|
E('button', { 'class': 'nm-btn nm-btn-primary', 'type': 'button', 'data-action': 'relay-save' }, ['💾 ', _('Save & Apply')]),
|
|
E('button', { 'class': 'nm-btn', 'type': 'button', 'data-action': 'relay-config' }, ['📝 ', _('Preview Config')])
|
|
]
|
|
});
|
|
|
|
var stats = E('div', { 'style': 'display:flex;flex-wrap:wrap;gap:12px;margin-bottom:24px;' }, [
|
|
helpers.createStatBadge({ label: _('Upstream'), value: (config.relay_interface || 'wlan0').toUpperCase() }),
|
|
helpers.createStatBadge({ label: _('LAN Bridge'), value: (config.lan_interface || 'br-lan').toUpperCase() }),
|
|
helpers.createStatBadge({ label: _('WireGuard'), value: wgConfig.enabled ? _('Enabled') : _('Disabled') }),
|
|
helpers.createStatBadge({ label: _('Relayd'), value: config.relayd_available ? _('Available') : _('Missing') })
|
|
]);
|
|
|
|
var relaySection = helpers.createSection({
|
|
title: _('Relay Interfaces'),
|
|
icon: '🔗',
|
|
badge: config.relayd_available ? _('Relayd ready') : _('Install relayd'),
|
|
body: [
|
|
E('div', { 'class': 'nm-form-grid' }, [
|
|
E('div', { 'class': 'nm-form-group' }, [
|
|
E('label', { 'class': 'nm-form-label' }, _('Upstream Interface')),
|
|
E('input', {
|
|
'class': 'nm-input',
|
|
'id': 'relay-interface',
|
|
'value': config.relay_interface || 'wlan0'
|
|
}),
|
|
E('div', { 'class': 'nm-form-hint' }, _('STA client connected to upstream WiFi or WAN'))
|
|
]),
|
|
E('div', { 'class': 'nm-form-group' }, [
|
|
E('label', { 'class': 'nm-form-label' }, _('LAN Interface / Bridge')),
|
|
E('input', {
|
|
'class': 'nm-input',
|
|
'id': 'lan-interface',
|
|
'value': config.lan_interface || 'br-lan'
|
|
}),
|
|
E('div', { 'class': 'nm-form-hint' }, _('Bridge containing downstream Ethernet/WiFi AP'))
|
|
])
|
|
]),
|
|
helpers.createList([
|
|
{ title: _('Relayd pseudo-bridge'), description: _('Extends upstream DHCP/NAT to LAN devices'), suffix: E('span', { 'class': 'nm-badge' }, _('Layer 3+2')) },
|
|
{ title: _('Per-mode backups'), description: _('Auto snapshot /etc/config/network before switching'), suffix: E('span', { 'class': 'nm-badge' }, _('Safety')) }
|
|
])
|
|
]
|
|
});
|
|
|
|
var wgSection = helpers.createSection({
|
|
title: _('WireGuard Backhaul'),
|
|
icon: '🔐',
|
|
badge: (config.wg_interfaces || []).length + ' ' + _('interfaces'),
|
|
body: [
|
|
E('div', { 'style': 'margin-bottom:12px;' }, [
|
|
E('button', { 'class': 'nm-btn', 'type': 'button', 'data-action': 'relay-generate-keys' }, '🔑 ' + _('Generate Keys')),
|
|
' ',
|
|
E('button', { 'class': 'nm-btn', 'type': 'button', 'data-action': 'relay-apply-wireguard' }, '🚀 ' + _('Deploy Interface'))
|
|
]),
|
|
E('div', { 'class': 'nm-toggle' }, [
|
|
E('div', { 'class': 'nm-toggle-info' }, [
|
|
E('span', { 'class': 'nm-toggle-icon' }, '🔐'),
|
|
E('div', {}, [
|
|
E('div', { 'class': 'nm-toggle-label' }, _('Enable WireGuard')),
|
|
E('div', { 'class': 'nm-toggle-desc' }, _('Route relay traffic through secure tunnel'))
|
|
])
|
|
]),
|
|
E('div', { 'class': 'nm-toggle-switch' + (wgConfig.enabled ? ' active' : ''), 'id': 'toggle-wg' })
|
|
]),
|
|
E('div', { 'class': 'nm-form-grid', 'style': 'margin-top:16px;' }, [
|
|
E('div', { 'class': 'nm-form-group' }, [
|
|
E('label', { 'class': 'nm-form-label' }, _('WireGuard Interface')),
|
|
E('select', { 'class': 'nm-select', 'id': 'wg-interface' },
|
|
(config.wg_interfaces || ['wg0']).map(function(iface) {
|
|
return E('option', {
|
|
'value': iface,
|
|
'selected': iface === wgConfig.interface
|
|
}, iface);
|
|
})
|
|
)
|
|
]),
|
|
E('div', { 'class': 'nm-form-group' }, [
|
|
E('label', { 'class': 'nm-form-label' }, _('Tunnel MTU')),
|
|
E('input', {
|
|
'class': 'nm-input',
|
|
'type': 'number',
|
|
'id': 'wg-mtu',
|
|
'value': wgConfig.mtu || 1420,
|
|
'min': '1280',
|
|
'max': '1500'
|
|
})
|
|
])
|
|
])
|
|
]
|
|
});
|
|
|
|
var optimSection = helpers.createSection({
|
|
title: _('Performance Optimizations'),
|
|
icon: '⚡',
|
|
body: [
|
|
buildOptToggle('mtu_optimization', '📏', _('MTU Optimization'), _('Auto-detect optimum MTU for relay path'), optConfig.mtu_optimization),
|
|
buildOptToggle('mss_clamping', '✂️', _('MSS Clamping'), _('Prevent TCP fragmentation issues'), optConfig.mss_clamping),
|
|
buildOptToggle('tcp_optimization', '🚀', _('TCP Acceleration'), _('Enable BBR and tcp_fastopen'), optConfig.tcp_optimization),
|
|
E('div', { 'class': 'nm-form-group', 'style': 'margin-top:16px;' }, [
|
|
E('label', { 'class': 'nm-form-label' }, _('Conntrack Max')),
|
|
E('input', {
|
|
'class': 'nm-input',
|
|
'type': 'number',
|
|
'id': 'conntrack-max',
|
|
'value': optConfig.conntrack_max || 16384,
|
|
'min': '1024',
|
|
'max': '262144'
|
|
}),
|
|
E('div', { 'class': 'nm-form-hint' }, _('Increase for busy relays (RAM usage grows accordingly)'))
|
|
]),
|
|
E('button', { 'class': 'nm-btn', 'type': 'button', 'data-action': 'relay-apply-optimizations' }, '⚡ ' + _('Run Optimization Tasks'))
|
|
]
|
|
});
|
|
|
|
var container = E('div', { 'class': 'network-modes-dashboard relay-mode' }, [
|
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('network-modes/dashboard.css') }),
|
|
helpers.createNavigationTabs('relay'),
|
|
hero,
|
|
stats,
|
|
relaySection,
|
|
wgSection,
|
|
optimSection
|
|
]);
|
|
|
|
container.querySelectorAll('.nm-toggle-switch').forEach(function(toggle) {
|
|
toggle.addEventListener('click', function() {
|
|
this.classList.toggle('active');
|
|
});
|
|
});
|
|
|
|
this.bindRelayActions(container);
|
|
return container;
|
|
},
|
|
|
|
bindRelayActions: function(container) {
|
|
var saveBtn = container.querySelector('[data-action="relay-save"]');
|
|
var configBtn = container.querySelector('[data-action="relay-config"]');
|
|
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 (saveBtn)
|
|
saveBtn.addEventListener('click', ui.createHandlerFn(this, 'saveRelaySettings', container));
|
|
if (configBtn)
|
|
configBtn.addEventListener('click', ui.createHandlerFn(helpers, helpers.showGeneratedConfig, 'relay'));
|
|
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');
|
|
});
|
|
}
|
|
});
|