fix: Use correct UCI section types in SecuBox settings view (v0.6.0-r12)
- Changed form sections from type 'secubox' to match actual UCI config - General/Dashboard/Module/Notification sections now use type 'core' - Alert Thresholds section now uses type 'diagnostics' - Security Settings section now uses type 'security' - Advanced Settings section uses type 'core' - Fixes "This section contains no values yet" errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0123ff005c
commit
9ce67f2da5
@ -263,7 +263,13 @@
|
||||
"Bash(netifyd:*)",
|
||||
"Bash(ubus call:*)",
|
||||
"Bash(ss:*)",
|
||||
"Bash(git cherry-pick:*)"
|
||||
"Bash(git cherry-pick:*)",
|
||||
"Bash(for file in overview.js decisions.js alerts.js waf.js metrics.js)",
|
||||
"Bash(do scp /home/reepost/CyberMindStudio/_files/secubox-openwrt/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/$file root@192.168.8.191:/www/luci-static/resources/view/crowdsec-dashboard/ done)",
|
||||
"Bash(Last: Xs ago\" in header\n- Updates every second\n- Visual feedback that polling is working\n- Easy to spot stalled/broken polling\n\nError Handling:\n- Try/catch around all poll callbacks\n- Errors logged to debug panel and console\n- Error counting for diagnostics\n- Polling continues even after errors\n\nCode Improvements:\n- Proper container creation order\n- Better error handling in load\\(\\) and polling\n- Debug logging throughout lifecycle\n- Performance metrics tracking\n\nDocumentation:\n- Complete analysis in REFRESH-DEBUG.md\n- Troubleshooting guide\n- Debug mode usage instructions\n- Performance considerations\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
|
||||
"Bash(ar r:*)",
|
||||
"Bash(git commit -m \"$\\(cat <<''EOF''\nfix: Add missing API utility functions and fix data structure handling \\(v0.6.0-r9\\)\n\n- Add parseScenario\\(\\) to format scenario names\n- Add getCountryFlag\\(\\) to display country flag emojis\n- Add formatRelativeTime\\(\\) for relative timestamps\n- Fix decisions data flattening in handleUnban, handleBulkUnban, submitBan, and polling\n- Fix getDashboardData to properly flatten alerts->decisions structure\n- Fix context error in overview renderDecisionsTable \\(this vs self\\)\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
|
||||
"Bash(git commit -m \"$\\(cat <<''EOF''\nfix: Sanitize malformed JSON from cscli metrics \\(v0.6.0-r10\\)\n\n- cscli metrics sometimes outputs empty string keys \\(\"\": {...}\\)\n- This causes RPC parsing errors in LuCI\n- Added sed filter to replace empty keys with \"unknown\"\n- Fixes \"No related RPC reply\" error in metrics view\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>\nEOF\n\\)\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -159,7 +159,7 @@ return view.extend({
|
||||
m = new form.Map('secubox', null, null);
|
||||
|
||||
// General Settings Section
|
||||
s = m.section(form.TypedSection, 'secubox', '🔧 General Settings');
|
||||
s = m.section(form.TypedSection, 'core', '🔧 General Settings');
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
@ -177,7 +177,7 @@ return view.extend({
|
||||
};
|
||||
|
||||
// Dashboard Settings Section
|
||||
s = m.section(form.TypedSection, 'secubox', '📊 Dashboard Settings');
|
||||
s = m.section(form.TypedSection, 'core', '📊 Dashboard Settings');
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
@ -289,7 +289,7 @@ return view.extend({
|
||||
o.default = '1';
|
||||
|
||||
// Module Management Section
|
||||
s = m.section(form.TypedSection, 'secubox', '📦 Module Management');
|
||||
s = m.section(form.TypedSection, 'core', '📦 Module Management');
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
@ -320,7 +320,7 @@ return view.extend({
|
||||
o.optional = true;
|
||||
|
||||
// Notification Settings Section
|
||||
s = m.section(form.TypedSection, 'secubox', '🔔 Notification Settings');
|
||||
s = m.section(form.TypedSection, 'core', '🔔 Notification Settings');
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
@ -349,7 +349,7 @@ return view.extend({
|
||||
o.depends('notifications', '1');
|
||||
|
||||
// Alert Thresholds Section
|
||||
s = m.section(form.TypedSection, 'secubox', '⚠️ Alert Thresholds');
|
||||
s = m.section(form.TypedSection, 'diagnostics', '⚠️ Alert Thresholds');
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
@ -390,7 +390,7 @@ return view.extend({
|
||||
o.placeholder = '85';
|
||||
|
||||
// Security Settings Section
|
||||
s = m.section(form.TypedSection, 'secubox', '🔒 Security Settings');
|
||||
s = m.section(form.TypedSection, 'security', '🔒 Security Settings');
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
@ -409,7 +409,7 @@ return view.extend({
|
||||
o.depends('audit_logging', '1');
|
||||
|
||||
// Advanced Settings Section
|
||||
s = m.section(form.TypedSection, 'secubox', '🛠️ Advanced Settings');
|
||||
s = m.section(form.TypedSection, 'core', '🛠️ Advanced Settings');
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
|
||||
@ -251,18 +251,32 @@ return view.extend({
|
||||
recommendedCollections.map(L.bind(function(collection) {
|
||||
var checkboxId = 'collection-' + collection.name.replace('/', '-');
|
||||
|
||||
return E('div', { 'class': 'collection-item' }, [
|
||||
E('label', { 'class': 'cyber-checkbox', 'for': checkboxId }, [
|
||||
E('input', {
|
||||
'type': 'checkbox',
|
||||
'id': checkboxId,
|
||||
'checked': collection.preselected ? 'checked' : null,
|
||||
'data-collection': collection.name
|
||||
}),
|
||||
E('div', { 'class': 'collection-info' }, [
|
||||
E('strong', {}, collection.name),
|
||||
E('div', { 'class': 'collection-desc' }, collection.description)
|
||||
])
|
||||
return E('div', {
|
||||
'class': 'collection-item',
|
||||
'data-collection': collection.name,
|
||||
'data-checked': collection.preselected ? '1' : '0',
|
||||
'style': 'display: flex; align-items: center; cursor: pointer;',
|
||||
'click': function(ev) {
|
||||
var item = ev.currentTarget;
|
||||
var currentState = item.getAttribute('data-checked') === '1';
|
||||
var newState = !currentState;
|
||||
item.setAttribute('data-checked', newState ? '1' : '0');
|
||||
|
||||
// Update visual indicator
|
||||
var checkbox = item.querySelector('.checkbox-indicator');
|
||||
if (checkbox) {
|
||||
checkbox.textContent = newState ? '☑' : '☐';
|
||||
checkbox.style.color = newState ? '#22c55e' : '#94a3b8';
|
||||
}
|
||||
}
|
||||
}, [
|
||||
E('span', {
|
||||
'class': 'checkbox-indicator',
|
||||
'style': 'display: inline-block; font-size: 28px; margin-right: 16px; user-select: none; color: ' + (collection.preselected ? '#22c55e' : '#94a3b8') + '; line-height: 1; min-width: 28px;'
|
||||
}, collection.preselected ? '☑' : '☐'),
|
||||
E('div', { 'class': 'collection-info', 'style': 'flex: 1;' }, [
|
||||
E('strong', {}, collection.name),
|
||||
E('div', { 'class': 'collection-desc' }, collection.description)
|
||||
])
|
||||
]);
|
||||
}, this))
|
||||
@ -281,17 +295,17 @@ return view.extend({
|
||||
E('button', {
|
||||
'class': 'cbi-button',
|
||||
'click': L.bind(this.goToStep, this, 2),
|
||||
'disabled': this.wizardData.installing
|
||||
'disabled': this.wizardData.installing ? true : null
|
||||
}, _('← Back')),
|
||||
E('button', {
|
||||
'class': 'cbi-button',
|
||||
'click': L.bind(this.goToStep, this, 4),
|
||||
'disabled': this.wizardData.installing
|
||||
'disabled': this.wizardData.installing ? true : null
|
||||
}, _('Skip')),
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-positive',
|
||||
'click': L.bind(this.handleInstallCollections, this),
|
||||
'disabled': this.wizardData.installing || this.wizardData.installed
|
||||
'disabled': (this.wizardData.installing || this.wizardData.installed) ? true : null
|
||||
}, this.wizardData.installed ? _('Installed ✓') : _('Install Selected'))
|
||||
])
|
||||
]);
|
||||
@ -304,27 +318,51 @@ return view.extend({
|
||||
|
||||
// Configuration options
|
||||
E('div', { 'class': 'config-section' }, [
|
||||
E('div', { 'class': 'config-group' }, [
|
||||
E('label', {}, [
|
||||
E('input', {
|
||||
'type': 'checkbox',
|
||||
'id': 'bouncer-ipv4',
|
||||
'checked': true
|
||||
}),
|
||||
' ',
|
||||
_('Enable IPv4 blocking')
|
||||
])
|
||||
E('div', {
|
||||
'class': 'config-group',
|
||||
'id': 'bouncer-ipv4',
|
||||
'data-checked': '1',
|
||||
'style': 'display: flex; align-items: center; cursor: pointer; padding: 12px; background: rgba(15, 23, 42, 0.5); border-radius: 8px; margin-bottom: 12px;',
|
||||
'click': function(ev) {
|
||||
var item = ev.currentTarget;
|
||||
var currentState = item.getAttribute('data-checked') === '1';
|
||||
var newState = !currentState;
|
||||
item.setAttribute('data-checked', newState ? '1' : '0');
|
||||
var checkbox = item.querySelector('.checkbox-indicator');
|
||||
if (checkbox) {
|
||||
checkbox.textContent = newState ? '☑' : '☐';
|
||||
checkbox.style.color = newState ? '#22c55e' : '#94a3b8';
|
||||
}
|
||||
}
|
||||
}, [
|
||||
E('span', {
|
||||
'class': 'checkbox-indicator',
|
||||
'style': 'display: inline-block; font-size: 24px; margin-right: 12px; user-select: none; color: #22c55e; min-width: 24px;'
|
||||
}, '☑'),
|
||||
E('span', {}, _('Enable IPv4 blocking'))
|
||||
]),
|
||||
E('div', { 'class': 'config-group' }, [
|
||||
E('label', {}, [
|
||||
E('input', {
|
||||
'type': 'checkbox',
|
||||
'id': 'bouncer-ipv6',
|
||||
'checked': true
|
||||
}),
|
||||
' ',
|
||||
_('Enable IPv6 blocking')
|
||||
])
|
||||
E('div', {
|
||||
'class': 'config-group',
|
||||
'id': 'bouncer-ipv6',
|
||||
'data-checked': '1',
|
||||
'style': 'display: flex; align-items: center; cursor: pointer; padding: 12px; background: rgba(15, 23, 42, 0.5); border-radius: 8px; margin-bottom: 12px;',
|
||||
'click': function(ev) {
|
||||
var item = ev.currentTarget;
|
||||
var currentState = item.getAttribute('data-checked') === '1';
|
||||
var newState = !currentState;
|
||||
item.setAttribute('data-checked', newState ? '1' : '0');
|
||||
var checkbox = item.querySelector('.checkbox-indicator');
|
||||
if (checkbox) {
|
||||
checkbox.textContent = newState ? '☑' : '☐';
|
||||
checkbox.style.color = newState ? '#22c55e' : '#94a3b8';
|
||||
}
|
||||
}
|
||||
}, [
|
||||
E('span', {
|
||||
'class': 'checkbox-indicator',
|
||||
'style': 'display: inline-block; font-size: 24px; margin-right: 12px; user-select: none; color: #22c55e; min-width: 24px;'
|
||||
}, '☑'),
|
||||
E('span', {}, _('Enable IPv6 blocking'))
|
||||
]),
|
||||
E('div', { 'class': 'config-group' }, [
|
||||
E('label', {}, _('Update Frequency:')),
|
||||
@ -344,7 +382,7 @@ return view.extend({
|
||||
]) :
|
||||
this.wizardData.configuring ?
|
||||
E('div', { 'class': 'spinning' }, _('Configuring bouncer...')) :
|
||||
null,
|
||||
E([]),
|
||||
|
||||
// API key display (if registered)
|
||||
this.wizardData.apiKey ?
|
||||
@ -359,14 +397,14 @@ return view.extend({
|
||||
ui.addNotification(null, E('p', _('API key copied')), 'info');
|
||||
}
|
||||
}, _('Copy'))
|
||||
]) : null,
|
||||
]) : E([]),
|
||||
|
||||
// Navigation
|
||||
E('div', { 'class': 'wizard-nav' }, [
|
||||
E('button', {
|
||||
'class': 'cbi-button',
|
||||
'click': L.bind(this.goToStep, this, 3),
|
||||
'disabled': this.wizardData.configuring
|
||||
'disabled': this.wizardData.configuring ? true : null
|
||||
}, _('← Back')),
|
||||
this.wizardData.bouncerConfigured ?
|
||||
E('button', {
|
||||
@ -375,8 +413,8 @@ return view.extend({
|
||||
}, _('Next →')) :
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'click': L.bind(this.handleConfigureBouncer, this),
|
||||
'disabled': this.wizardData.configuring
|
||||
'click': L.bind(function(ev) { console.log('[Wizard] Configure Bouncer button clicked!'); ev.preventDefault(); ev.stopPropagation(); this.handleConfigureBouncer(); }, this),
|
||||
'disabled': this.wizardData.configuring ? true : null
|
||||
}, _('Configure Bouncer'))
|
||||
])
|
||||
]);
|
||||
@ -416,7 +454,7 @@ return view.extend({
|
||||
E('button', {
|
||||
'class': 'cbi-button',
|
||||
'click': L.bind(this.goToStep, this, 4),
|
||||
'disabled': this.wizardData.starting
|
||||
'disabled': this.wizardData.starting ? true : null
|
||||
}, _('← Back')),
|
||||
(this.wizardData.enabled && this.wizardData.running && this.wizardData.nftablesActive && this.wizardData.lapiConnected) ?
|
||||
E('button', {
|
||||
@ -425,8 +463,8 @@ return view.extend({
|
||||
}, _('Next →')) :
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'click': L.bind(this.handleStartServices, this),
|
||||
'disabled': this.wizardData.starting
|
||||
'click': L.bind(function(ev) { console.log('[Wizard] Start Services button clicked!'); ev.preventDefault(); ev.stopPropagation(); this.handleStartServices(); }, this),
|
||||
'disabled': this.wizardData.starting ? true : null
|
||||
}, _('Start Services'))
|
||||
])
|
||||
]);
|
||||
@ -577,10 +615,13 @@ return view.extend({
|
||||
},
|
||||
|
||||
handleInstallCollections: function() {
|
||||
var checkboxes = document.querySelectorAll('[data-collection]');
|
||||
var selected = Array.from(checkboxes)
|
||||
.filter(function(cb) { return cb.checked; })
|
||||
.map(function(cb) { return cb.dataset.collection; });
|
||||
// Read from data-checked attributes (Unicode checkbox approach)
|
||||
var items = document.querySelectorAll('.collection-item[data-collection]');
|
||||
var selected = Array.from(items)
|
||||
.filter(function(item) { return item.getAttribute('data-checked') === '1'; })
|
||||
.map(function(item) { return item.getAttribute('data-collection'); });
|
||||
|
||||
console.log('[Wizard] Selected collections:', selected);
|
||||
|
||||
if (selected.length === 0) {
|
||||
this.goToStep(4);
|
||||
@ -617,15 +658,22 @@ return view.extend({
|
||||
},
|
||||
|
||||
handleConfigureBouncer: function() {
|
||||
console.log('[Wizard] handleConfigureBouncer called');
|
||||
this.wizardData.configuring = true;
|
||||
this.refreshView();
|
||||
|
||||
var ipv4 = document.getElementById('bouncer-ipv4').checked;
|
||||
var ipv6 = document.getElementById('bouncer-ipv6').checked;
|
||||
var ipv4Elem = document.getElementById('bouncer-ipv4');
|
||||
var ipv6Elem = document.getElementById('bouncer-ipv6');
|
||||
var ipv4 = ipv4Elem ? ipv4Elem.getAttribute('data-checked') === '1' : true;
|
||||
var ipv6 = ipv6Elem ? ipv6Elem.getAttribute('data-checked') === '1' : true;
|
||||
var frequency = document.getElementById('bouncer-frequency').value;
|
||||
|
||||
console.log('[Wizard] Bouncer config:', { ipv4: ipv4, ipv6: ipv6, frequency: frequency });
|
||||
|
||||
// Step 1: Register bouncer
|
||||
console.log('[Wizard] Registering bouncer...');
|
||||
return API.registerBouncer('crowdsec-firewall-bouncer').then(L.bind(function(result) {
|
||||
console.log('[Wizard] registerBouncer result:', result);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Bouncer registration failed');
|
||||
}
|
||||
@ -642,15 +690,18 @@ return view.extend({
|
||||
];
|
||||
|
||||
return Promise.all(configPromises);
|
||||
}, this)).then(L.bind(function() {
|
||||
}, this)).then(L.bind(function(results) {
|
||||
console.log('[Wizard] UCI config results:', results);
|
||||
this.wizardData.configuring = false;
|
||||
this.wizardData.bouncerConfigured = true;
|
||||
ui.addNotification(null, E('p', _('Bouncer configured successfully')), 'info');
|
||||
this.refreshView();
|
||||
|
||||
// Auto-advance after 2 seconds
|
||||
console.log('[Wizard] Auto-advancing to Step 5 in 2 seconds...');
|
||||
setTimeout(L.bind(function() { this.goToStep(5); }, this), 2000);
|
||||
}, this)).catch(L.bind(function(err) {
|
||||
console.error('[Wizard] Configuration error:', err);
|
||||
this.wizardData.configuring = false;
|
||||
ui.addNotification(null, E('p', _('Configuration failed: %s').format(err.message)), 'error');
|
||||
this.refreshView();
|
||||
@ -658,50 +709,71 @@ return view.extend({
|
||||
},
|
||||
|
||||
handleStartServices: function() {
|
||||
console.log('[Wizard] handleStartServices called');
|
||||
this.wizardData.starting = true;
|
||||
this.wizardData.enabling = true;
|
||||
this.refreshView();
|
||||
|
||||
// Step 1: Enable service
|
||||
console.log('[Wizard] Enabling firewall bouncer...');
|
||||
return API.controlFirewallBouncer('enable').then(L.bind(function(result) {
|
||||
console.log('[Wizard] Enable result:', result);
|
||||
this.wizardData.enabling = false;
|
||||
this.wizardData.enabled = result.success;
|
||||
this.refreshView();
|
||||
|
||||
// Step 2: Start service
|
||||
console.log('[Wizard] Starting firewall bouncer...');
|
||||
return API.controlFirewallBouncer('start');
|
||||
}, this)).then(L.bind(function(result) {
|
||||
console.log('[Wizard] Start result:', result);
|
||||
this.wizardData.running = result.success;
|
||||
this.refreshView();
|
||||
|
||||
// Step 3: Wait 3 seconds for service to initialize
|
||||
console.log('[Wizard] Waiting 3 seconds for service initialization...');
|
||||
return new Promise(function(resolve) { setTimeout(resolve, 3000); });
|
||||
}, this)).then(L.bind(function() {
|
||||
// Step 4: Check status
|
||||
console.log('[Wizard] Checking firewall bouncer status...');
|
||||
return API.getFirewallBouncerStatus();
|
||||
}, this)).then(L.bind(function(status) {
|
||||
console.log('[Wizard] Bouncer status:', status);
|
||||
this.wizardData.nftablesActive = status.nftables_ipv4 || status.nftables_ipv6;
|
||||
this.wizardData.starting = false;
|
||||
|
||||
// Step 5: Verify LAPI connection (check if bouncer pulled decisions)
|
||||
console.log('[Wizard] Checking LAPI connection...');
|
||||
return API.getBouncers();
|
||||
}, this)).then(L.bind(function(bouncers) {
|
||||
console.log('[Wizard] Bouncers list:', bouncers);
|
||||
var bouncer = (bouncers || []).find(function(b) {
|
||||
return b.name === 'crowdsec-firewall-bouncer';
|
||||
});
|
||||
|
||||
this.wizardData.lapiConnected = bouncer && bouncer.last_pull;
|
||||
console.log('[Wizard] Final status:', {
|
||||
enabled: this.wizardData.enabled,
|
||||
running: this.wizardData.running,
|
||||
nftablesActive: this.wizardData.nftablesActive,
|
||||
lapiConnected: this.wizardData.lapiConnected
|
||||
});
|
||||
this.refreshView();
|
||||
|
||||
if (this.wizardData.enabled && this.wizardData.running &&
|
||||
this.wizardData.nftablesActive && this.wizardData.lapiConnected) {
|
||||
// Success if enabled, running, and nftables active
|
||||
// LAPI connection may take a few seconds to establish, so it's optional
|
||||
if (this.wizardData.enabled && this.wizardData.running &&
|
||||
this.wizardData.nftablesActive) {
|
||||
console.log('[Wizard] All critical services started! Auto-advancing to Step 6...');
|
||||
ui.addNotification(null, E('p', _('Services started successfully!')), 'info');
|
||||
// Auto-advance after 2 seconds
|
||||
setTimeout(L.bind(function() { this.goToStep(6); }, this), 2000);
|
||||
} else {
|
||||
console.log('[Wizard] Service startup incomplete');
|
||||
ui.addNotification(null, E('p', _('Service startup incomplete. Check status and retry.')), 'warning');
|
||||
}
|
||||
}, this)).catch(L.bind(function(err) {
|
||||
console.error('[Wizard] Service startup error:', err);
|
||||
this.wizardData.starting = false;
|
||||
ui.addNotification(null, E('p', _('Service start failed: %s').format(err.message)), 'error');
|
||||
this.refreshView();
|
||||
|
||||
@ -213,6 +213,14 @@ Netifyd streams JSON data via:
|
||||
}
|
||||
```
|
||||
|
||||
## Flow Plugin Integration
|
||||
|
||||
SecuBox can emit the plugin configurations referenced in the Netify.ai examples for tagging BitTorrent traffic with IP sets and pushing verdicts into nftables. After copying the relevant Netify plugin binaries into `/usr/lib/netifyd/`, open the **Flow Export → Flow Plugins** section in LuCI to enable the `mark-bittorrent-with-ip-sets` and `block-traffic-with-nftables` templates. Hit **Apply Flow Plugins** to regenerate `/etc/netifyd/plugins.d/secubox-*.conf` and restart Netifyd so the new ipsets and nftables chains are activated.
|
||||
|
||||
Refer to the upstream examples for exact `ipset`/`chain` rules:
|
||||
- https://www.netify.ai/documentation/netify-plugins/v5/examples/mark-bittorrent-with-ip-sets
|
||||
- https://www.netify.ai/documentation/netify-plugins/v5/examples/block-traffic-with-nftables
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Netifyd Not Starting
|
||||
|
||||
@ -191,6 +191,16 @@ return baseclass.extend({
|
||||
})(format || 'json'), {});
|
||||
},
|
||||
|
||||
/**
|
||||
* Apply plugin configuration and restart Netifyd
|
||||
*/
|
||||
applyPluginConfig: function() {
|
||||
return L.resolveDefault(rpc.declare({
|
||||
object: 'luci.secubox-netifyd',
|
||||
method: 'apply_plugin_configuration'
|
||||
})(), {});
|
||||
},
|
||||
|
||||
/**
|
||||
* Format bytes to human-readable format
|
||||
*/
|
||||
|
||||
@ -12,6 +12,30 @@ return view.extend({
|
||||
trendsContainer: null,
|
||||
isPaused: false,
|
||||
|
||||
formatInterfaceLabel: function(name) {
|
||||
if (!name) {
|
||||
return _('Unknown');
|
||||
}
|
||||
|
||||
if (typeof name === 'string') {
|
||||
return name;
|
||||
}
|
||||
|
||||
if (name.nodeType && typeof name.textContent === 'string') {
|
||||
return name.textContent.trim();
|
||||
}
|
||||
|
||||
return String(name);
|
||||
},
|
||||
|
||||
normalizePacketPercentage: function(value, total) {
|
||||
if (!total || total <= 0) {
|
||||
return '0.0';
|
||||
}
|
||||
|
||||
return (value / total * 100).toFixed(1);
|
||||
},
|
||||
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
netifydAPI.getDashboard(),
|
||||
@ -95,8 +119,13 @@ return view.extend({
|
||||
},
|
||||
|
||||
renderInterfaceFlows: function(interfaces, stats) {
|
||||
var self = this;
|
||||
|
||||
if (!interfaces || Object.keys(interfaces).length === 0) {
|
||||
return null;
|
||||
return E('div', {
|
||||
'class': 'alert-message info',
|
||||
'style': 'text-align: center; padding: 1.5rem; margin-bottom: 1rem'
|
||||
}, _('No interface activity detected yet'));
|
||||
}
|
||||
|
||||
var interfaceList = [];
|
||||
@ -139,7 +168,7 @@ return view.extend({
|
||||
E('div', { 'class': 'td', 'style': 'width: 25%' }, [
|
||||
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5rem' }, [
|
||||
E('i', { 'class': 'fa fa-ethernet', 'style': 'color: ' + (isActive ? '#3b82f6' : '#9ca3af') }),
|
||||
E('strong', iface.name)
|
||||
E('strong', self.formatInterfaceLabel(iface.name))
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'td center', 'style': 'width: 15%' }, [
|
||||
@ -187,6 +216,8 @@ return view.extend({
|
||||
renderProtocolBreakdown: function(stats) {
|
||||
if (!stats) return null;
|
||||
|
||||
var self = this;
|
||||
|
||||
var tcp = stats.tcp_packets || 0;
|
||||
var udp = stats.udp_packets || 0;
|
||||
var icmp = stats.icmp_packets || 0;
|
||||
@ -203,21 +234,21 @@ return view.extend({
|
||||
{
|
||||
name: 'TCP',
|
||||
packets: tcp,
|
||||
percentage: (tcp / total * 100).toFixed(1),
|
||||
percentage: self.normalizePacketPercentage(tcp, total),
|
||||
color: '#3b82f6',
|
||||
icon: 'exchange-alt'
|
||||
},
|
||||
{
|
||||
name: 'UDP',
|
||||
packets: udp,
|
||||
percentage: (udp / total * 100).toFixed(1),
|
||||
percentage: self.normalizePacketPercentage(udp, total),
|
||||
color: '#10b981',
|
||||
icon: 'paper-plane'
|
||||
},
|
||||
{
|
||||
name: 'ICMP',
|
||||
packets: icmp,
|
||||
percentage: (icmp / total * 100).toFixed(1),
|
||||
percentage: self.normalizePacketPercentage(icmp, total),
|
||||
color: '#f59e0b',
|
||||
icon: 'broadcast-tower'
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require rpc';
|
||||
'require secubox-netifyd/api as netifydAPI';
|
||||
|
||||
var callServiceList = rpc.declare({
|
||||
object: 'service',
|
||||
@ -214,6 +215,124 @@ return view.extend({
|
||||
o.datatype = 'port';
|
||||
o.depends('type', 'tcp');
|
||||
|
||||
// ========== Flow Plugin Templates ==========
|
||||
s = m.section(form.NamedSection, 'bittorrent', 'plugin',
|
||||
E('span', [
|
||||
E('i', { 'class': 'fa fa-hashtag', 'style': 'margin-right: 0.5rem' }),
|
||||
_('BitTorrent IP Set')
|
||||
]),
|
||||
_('Mark BitTorrent flows with an ipset so nftables rules can react to them.'));
|
||||
s.addremove = false;
|
||||
|
||||
o = s.option(form.Flag, 'enabled',
|
||||
E('span', [
|
||||
E('i', { 'class': 'fa fa-toggle-on', 'style': 'margin-right: 0.5rem' }),
|
||||
_('Enable Plugin')
|
||||
]),
|
||||
_('Generate the Netify plugin config described in the BitTorrent IP set example.')
|
||||
);
|
||||
o.default = '0';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'ipset',
|
||||
E('span', [
|
||||
E('i', { 'class': 'fa fa-database', 'style': 'margin-right: 0.5rem' }),
|
||||
_('Target IP Set')
|
||||
]),
|
||||
_('IP set name used to tag BitTorrent traffic.')
|
||||
);
|
||||
o.default = 'secubox-bittorrent';
|
||||
|
||||
o = s.option(form.ListValue, 'ipset_family',
|
||||
E('span', [
|
||||
E('i', { 'class': 'fa fa-globe', 'style': 'margin-right: 0.5rem' }),
|
||||
_('IP Family')
|
||||
]),
|
||||
_('IP set family used by the plugin (inet or inet6).')
|
||||
);
|
||||
o.value('inet', _('IPv4 (inet)'));
|
||||
o.value('inet6', _('IPv6 (inet6)'));
|
||||
o.default = 'inet';
|
||||
|
||||
o = s.option(form.Value, 'match_application',
|
||||
E('span', [
|
||||
E('i', { 'class': 'fa fa-search', 'style': 'margin-right: 0.5rem' }),
|
||||
_('Match Application')
|
||||
]),
|
||||
_('Application identifier that triggers the IP set entry (default: bittorrent).')
|
||||
);
|
||||
o.default = 'bittorrent';
|
||||
|
||||
s = m.section(form.NamedSection, 'nftables', 'plugin',
|
||||
E('span', [
|
||||
E('i', { 'class': 'fa fa-fire', 'style': 'margin-right: 0.5rem' }),
|
||||
_('nftables Verdicts')
|
||||
]),
|
||||
_('Emit flow verdicts into nftables chains after the Netify block-traffic example.'));
|
||||
s.addremove = false;
|
||||
|
||||
o = s.option(form.Flag, 'enabled',
|
||||
E('span', [
|
||||
E('i', { 'class': 'fa fa-toggle-on', 'style': 'margin-right: 0.5rem' }),
|
||||
_('Enable Plugin')
|
||||
]),
|
||||
_('Generate the Netify plugin config described in the nftables example.')
|
||||
);
|
||||
o.default = '0';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'table',
|
||||
E('span', [
|
||||
E('i', { 'class': 'fa fa-table', 'style': 'margin-right: 0.5rem' }),
|
||||
_('nftables Table')
|
||||
]),
|
||||
_('Table where the plugin will insert verdicts.')
|
||||
);
|
||||
o.default = 'filter';
|
||||
|
||||
o = s.option(form.Value, 'chain',
|
||||
E('span', [
|
||||
E('i', { 'class': 'fa fa-chain', 'style': 'margin-right: 0.5rem' }),
|
||||
_('nftables Chain')
|
||||
]),
|
||||
_('Chain used by the verdicts.')
|
||||
);
|
||||
o.default = 'SECUBOX';
|
||||
|
||||
o = s.option(form.Value, 'action',
|
||||
E('span', [
|
||||
E('i', { 'class': 'fa fa-ban', 'style': 'margin-right: 0.5rem' }),
|
||||
_('Action')
|
||||
]),
|
||||
_('Action applied when the plugin matches a flow (drop/reject/queue).')
|
||||
);
|
||||
o.default = 'drop';
|
||||
|
||||
o = s.option(form.Value, 'target_ipset',
|
||||
E('span', [
|
||||
E('i', { 'class': 'fa fa-database', 'style': 'margin-right: 0.5rem' }),
|
||||
_('Target IP Set')
|
||||
]),
|
||||
_('nftables ipset referenced by the verdict chain.')
|
||||
);
|
||||
o.default = 'secubox-banned';
|
||||
|
||||
o = s.option(form.Button, 'apply_plugins',
|
||||
E('span', [
|
||||
E('i', { 'class': 'fa fa-sync', 'style': 'margin-right: 0.5rem' }),
|
||||
_('Apply Flow Plugins')
|
||||
]),
|
||||
_('Regenerate plugin configs and restart Netifyd.')
|
||||
);
|
||||
o.inputstyle = 'action';
|
||||
o.write = function() {
|
||||
netifydAPI.applyPluginConfig().then(function(result) {
|
||||
ui.addNotification({ type: 'success', description: result.message || _('Plugin configuration applied') });
|
||||
}, function(err) {
|
||||
ui.addNotification({ type: 'error', description: (err && err.error && err.error.message) || _('Plugin configuration failed') });
|
||||
});
|
||||
};
|
||||
|
||||
// ========== Monitoring Settings Section ==========
|
||||
s = m.section(form.TypedSection, 'monitoring',
|
||||
E('span', [
|
||||
|
||||
@ -35,3 +35,16 @@ config sink 'sink'
|
||||
option unix_path '/tmp/netifyd-flows.json'
|
||||
option tcp_address '127.0.0.1'
|
||||
option tcp_port '9501'
|
||||
|
||||
config plugin 'bittorrent'
|
||||
option enabled '0'
|
||||
option ipset 'secubox-bittorrent'
|
||||
option ipset_family 'inet'
|
||||
option match_application 'bittorrent'
|
||||
|
||||
config plugin 'nftables'
|
||||
option enabled '0'
|
||||
option table 'filter'
|
||||
option chain 'SECUBOX'
|
||||
option action 'drop'
|
||||
option target_ipset 'secubox-banned'
|
||||
|
||||
@ -16,6 +16,9 @@ LOG_FILE="/var/log/secubox-netifyd.log"
|
||||
FLOW_CACHE="/tmp/netifyd-flows.json"
|
||||
STATS_CACHE="/tmp/netifyd-stats.json"
|
||||
NETIFYD_SINK_CONF="/etc/netifyd.d/secubox-sink.conf"
|
||||
NETIFYD_PLUGIN_LIBDIR="/usr/lib/netifyd"
|
||||
NETIFYD_PLUGIN_CONF_DIR="/etc/netifyd/plugins.d"
|
||||
NETIFYD_STATE_DIR="/etc/netify.d"
|
||||
|
||||
# Logging function
|
||||
log_msg() {
|
||||
@ -69,6 +72,60 @@ EOF
|
||||
fi
|
||||
}
|
||||
|
||||
apply_plugin_config() {
|
||||
local plugin_dir="$NETIFYD_PLUGIN_CONF_DIR"
|
||||
local bittorrent_conf="$plugin_dir/secubox-bittorrent-ipset.conf"
|
||||
local nft_conf="$plugin_dir/secubox-nftables-block.conf"
|
||||
|
||||
mkdir -p "$plugin_dir"
|
||||
mkdir -p "$NETIFYD_STATE_DIR"
|
||||
|
||||
local bittorrent_enabled=$(uci -q get secubox-netifyd.bittorrent.enabled || echo 0)
|
||||
local ipset_name=$(uci -q get secubox-netifyd.bittorrent.ipset || echo 'secubox-bittorrent')
|
||||
local ipset_family=$(uci -q get secubox-netifyd.bittorrent.ipset_family || echo 'inet')
|
||||
local match_app=$(uci -q get secubox-netifyd.bittorrent.match_application || echo 'bittorrent')
|
||||
|
||||
if [ "$bittorrent_enabled" -eq 1 ]; then
|
||||
cat <<EOF > "$bittorrent_conf"
|
||||
[secubox-bittorrent-ipset]
|
||||
enable = yes
|
||||
plugin_library = ${NETIFYD_PLUGIN_LIBDIR}/libnetify-plugin-bittorrent-ipset.so
|
||||
conf_filename = ${NETIFYD_STATE_DIR}/secubox-bittorrent-ipset.json
|
||||
ipset-name = $ipset_name
|
||||
ipset-family = $ipset_family
|
||||
match-applications = $match_app
|
||||
ipset-timeout = 900
|
||||
EOF
|
||||
log_msg "INFO" "BitTorrent ipset plugin enabled ($ipset_name)"
|
||||
else
|
||||
rm -f "$bittorrent_conf"
|
||||
log_msg "INFO" "BitTorrent ipset plugin disabled"
|
||||
fi
|
||||
|
||||
local nft_enabled=$(uci -q get secubox-netifyd.nftables.enabled || echo 0)
|
||||
local nft_table=$(uci -q get secubox-netifyd.nftables.table || echo 'filter')
|
||||
local nft_chain=$(uci -q get secubox-netifyd.nftables.chain || echo 'SECUBOX')
|
||||
local nft_action=$(uci -q get secubox-netifyd.nftables.action || echo 'drop')
|
||||
local nft_ipset=$(uci -q get secubox-netifyd.nftables.target_ipset || echo 'secubox-banned')
|
||||
|
||||
if [ "$nft_enabled" -eq 1 ]; then
|
||||
cat <<EOF > "$nft_conf"
|
||||
[secubox-nftables-block]
|
||||
enable = yes
|
||||
plugin_library = ${NETIFYD_PLUGIN_LIBDIR}/libnetify-plugin-nftables-block.so
|
||||
conf_filename = ${NETIFYD_STATE_DIR}/secubox-nftables-block.json
|
||||
table = $nft_table
|
||||
chain = $nft_chain
|
||||
action = $nft_action
|
||||
ipset = $nft_ipset
|
||||
EOF
|
||||
log_msg "INFO" "nftables plugin enabled ($nft_chain@$nft_table -> $nft_action $nft_ipset)"
|
||||
else
|
||||
rm -f "$nft_conf"
|
||||
log_msg "INFO" "nftables plugin disabled"
|
||||
fi
|
||||
}
|
||||
|
||||
# Get netifyd service status
|
||||
get_service_status() {
|
||||
json_init
|
||||
@ -681,6 +738,26 @@ get_config() {
|
||||
json_add_int "alert_threshold_mbps" "$(uci -q get secubox-netifyd.alerts.alert_threshold_mbps || echo 100)"
|
||||
json_close_object
|
||||
|
||||
# Plugins
|
||||
json_add_object "plugins"
|
||||
|
||||
json_add_object "bittorrent"
|
||||
json_add_boolean "enabled" "$(uci -q get secubox-netifyd.bittorrent.enabled || echo 0)"
|
||||
json_add_string "ipset" "$(uci -q get secubox-netifyd.bittorrent.ipset || echo 'secubox-bittorrent')"
|
||||
json_add_string "ipset_family" "$(uci -q get secubox-netifyd.bittorrent.ipset_family || echo 'inet')"
|
||||
json_add_string "match_application" "$(uci -q get secubox-netifyd.bittorrent.match_application || echo 'bittorrent')"
|
||||
json_close_object
|
||||
|
||||
json_add_object "nftables"
|
||||
json_add_boolean "enabled" "$(uci -q get secubox-netifyd.nftables.enabled || echo 0)"
|
||||
json_add_string "table" "$(uci -q get secubox-netifyd.nftables.table || echo 'filter')"
|
||||
json_add_string "chain" "$(uci -q get secubox-netifyd.nftables.chain || echo 'SECUBOX')"
|
||||
json_add_string "action" "$(uci -q get secubox-netifyd.nftables.action || echo 'drop')"
|
||||
json_add_string "target_ipset" "$(uci -q get secubox-netifyd.nftables.target_ipset || echo 'secubox-banned')"
|
||||
json_close_object
|
||||
|
||||
json_close_object
|
||||
|
||||
json_dump
|
||||
}
|
||||
|
||||
@ -739,9 +816,10 @@ update_config() {
|
||||
uci commit secubox-netifyd
|
||||
|
||||
apply_flow_sink_config
|
||||
apply_plugin_config
|
||||
if check_netifyd_installed; then
|
||||
/etc/init.d/netifyd restart >/dev/null 2>&1 || true
|
||||
log_msg "INFO" "Netifyd restarted to apply sink configuration"
|
||||
log_msg "INFO" "Netifyd restarted to apply sink and plugin configuration"
|
||||
fi
|
||||
|
||||
log_msg "INFO" "Configuration updated"
|
||||
@ -751,6 +829,22 @@ update_config() {
|
||||
json_dump
|
||||
}
|
||||
|
||||
apply_plugin_configuration() {
|
||||
json_init
|
||||
|
||||
apply_plugin_config
|
||||
if check_netifyd_installed; then
|
||||
/etc/init.d/netifyd restart >/dev/null 2>&1 || true
|
||||
log_msg "INFO" "Netifyd restarted after plugin configuration sync"
|
||||
else
|
||||
log_msg "WARN" "Netifyd binary not found; plugin configuration stored"
|
||||
fi
|
||||
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "Plugin configuration synced"
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Get network interfaces being monitored
|
||||
get_interfaces() {
|
||||
json_init
|
||||
@ -860,6 +954,7 @@ case "$1" in
|
||||
"alerts": "object",
|
||||
"sink": "object"
|
||||
},
|
||||
"apply_plugin_configuration": {},
|
||||
"get_interfaces": {},
|
||||
"clear_cache": {},
|
||||
"export_flows": {
|
||||
@ -885,6 +980,7 @@ EOF
|
||||
service_disable) service_disable ;;
|
||||
get_config) get_config ;;
|
||||
update_config) update_config ;;
|
||||
apply_plugin_configuration) apply_plugin_configuration ;;
|
||||
get_interfaces) get_interfaces ;;
|
||||
clear_cache) clear_cache ;;
|
||||
export_flows) export_flows ;;
|
||||
|
||||
@ -104,6 +104,14 @@ if [ ! -f "$FLOW_EXPORT" ]; then
|
||||
echo " # Your agent UUID: $(jq -r '.agent_uuid // "not-set"' "$NETIFYD_STATUS" 2>/dev/null)"
|
||||
echo " # Dashboard: https://dashboard.netify.ai"
|
||||
echo ""
|
||||
echo "Method 4: Sync Flow Plugins (BitTorrent IP set / nftables block)"
|
||||
echo " # Enable the Flow Plugins section in LuCI, then run:"
|
||||
echo " ubus call luci.secubox-netifyd apply_plugin_configuration '{}'"
|
||||
echo " # Plugin configs live under /etc/netifyd/plugins.d/secubox-*.conf"
|
||||
echo " # See:"
|
||||
echo " # - https://www.netify.ai/documentation/netify-plugins/v5/examples/mark-bittorrent-with-ip-sets"
|
||||
echo " # - https://www.netify.ai/documentation/netify-plugins/v5/examples/block-traffic-with-nftables"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Check if sink is enabled
|
||||
|
||||
@ -32,6 +32,7 @@
|
||||
"service_enable",
|
||||
"service_disable",
|
||||
"update_config",
|
||||
"apply_plugin_configuration",
|
||||
"clear_cache",
|
||||
"export_flows"
|
||||
]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user