From 50bd0c872e33efc564fe484ba3b0056c75fdf248 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Fri, 9 Jan 2026 12:43:01 +0100 Subject: [PATCH] feat: Enhance monitoring page layout and fix nDPId detailed flows Monitoring page: - Move Current Statistics card above histogram charts - Replace Network Throughput with System Load chart - Fix API field mapping (usage_percent vs percent) - Parse load from cpu.load string format nDPId app: - Add get_detailed_flows and get_categories RPCD methods - Fix subshell variable scope bug in RPCD script - Add interface scanning from /sys/class/net - Update ACL permissions for new methods - Enhance flows.js with Array.isArray data handling Co-Authored-By: Claude Opus 4.5 --- .../resources/view/secubox/monitoring.js | 66 ++- package/secubox/luci-app-ndpid/Makefile | 4 +- .../htdocs/luci-static/resources/ndpid/api.js | 14 + .../luci-static/resources/ndpid/dashboard.css | 273 +++++++++++ .../resources/view/ndpid/dashboard.js | 167 +++++-- .../luci-static/resources/view/ndpid/flows.js | 428 +++++++++++++----- .../root/usr/bin/ndpid-collector | 0 .../luci-app-ndpid/root/usr/bin/ndpid-compat | 215 +++++++-- .../root/usr/bin/ndpid-flow-actions | 0 .../root/usr/libexec/rpcd/luci.ndpid | 163 ++++++- .../usr/share/rpcd/acl.d/luci-app-ndpid.json | 2 + 11 files changed, 1120 insertions(+), 212 deletions(-) mode change 100644 => 100755 package/secubox/luci-app-ndpid/root/usr/bin/ndpid-collector mode change 100644 => 100755 package/secubox/luci-app-ndpid/root/usr/bin/ndpid-flow-actions diff --git a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/monitoring.js b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/monitoring.js index a0e1d1ab..b8ba48d7 100644 --- a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/monitoring.js +++ b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/monitoring.js @@ -29,6 +29,7 @@ return view.extend({ cpuHistory: [], memoryHistory: [], diskHistory: [], + loadHistory: [], networkHistory: [], maxDataPoints: 60, latestHealth: {}, @@ -50,15 +51,23 @@ return view.extend({ addDataPoint: function(health) { var timestamp = Date.now(); - this.cpuHistory.push({ time: timestamp, value: (health.cpu && health.cpu.percent) || 0 }); - this.memoryHistory.push({ time: timestamp, value: (health.memory && health.memory.percent) || 0 }); - this.diskHistory.push({ time: timestamp, value: (health.disk && health.disk.percent) || 0 }); + // API returns usage_percent, not percent + this.cpuHistory.push({ time: timestamp, value: (health.cpu && (health.cpu.usage_percent || health.cpu.percent)) || 0 }); + this.memoryHistory.push({ time: timestamp, value: (health.memory && (health.memory.usage_percent || health.memory.percent)) || 0 }); + this.diskHistory.push({ time: timestamp, value: (health.disk && (health.disk.usage_percent || health.disk.percent)) || 0 }); + + // System load - parse from string "2.14 1.86 1.70" or array, scale to percentage (assume 4 cores = 400% max) + var loadStr = (health.cpu && health.cpu.load) || (health.load && (Array.isArray(health.load) ? health.load[0] : health.load)) || '0'; + var loadAvg = Array.isArray(loadStr) ? loadStr[0] : (typeof loadStr === 'string' ? parseFloat(loadStr.split(' ')[0]) : loadStr); + var numCores = (health.cpu && health.cpu.count) || 4; + var loadPercent = Math.min(100, (parseFloat(loadAvg) / numCores) * 100); + this.loadHistory.push({ time: timestamp, value: loadPercent, raw: loadAvg }); var netRx = (health.network && health.network.rx_bytes) || 0; var netTx = (health.network && health.network.tx_bytes) || 0; this.networkHistory.push({ time: timestamp, rx: netRx, tx: netTx }); - ['cpuHistory', 'memoryHistory', 'diskHistory', 'networkHistory'].forEach(function(key) { + ['cpuHistory', 'memoryHistory', 'diskHistory', 'loadHistory', 'networkHistory'].forEach(function(key) { if (this[key].length > this.maxDataPoints) this[key].shift(); }, this); @@ -73,8 +82,8 @@ return view.extend({ E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox/monitoring.css') }), SecuNav.renderTabs('monitoring'), this.renderHeader(), - this.renderChartsGrid(), - this.renderCurrentStatsCard() + this.renderCurrentStatsCard(), + this.renderChartsGrid() ]); this.updateCharts(); @@ -131,7 +140,7 @@ return view.extend({ this.renderChartCard('cpu', _('CPU Usage'), '%', '#6366f1'), this.renderChartCard('memory', _('Memory Usage'), '%', '#22c55e'), this.renderChartCard('disk', _('Disk Usage'), '%', '#f59e0b'), - this.renderChartCard('network', _('Network Throughput'), 'B/s', '#3b82f6') + this.renderChartCard('load', _('System Load'), '', '#ec4899') ]); }, @@ -167,7 +176,10 @@ return view.extend({ renderStatsTable: function() { var snapshot = this.getLatestSnapshot(); var rates = this.getNetworkRateSummary(); - var load = (this.latestHealth.load && this.latestHealth.load[0]) || '0.00'; + // Load from cpu.load string "2.14 1.86 1.70" or load array + var loadStr = (this.latestHealth.cpu && this.latestHealth.cpu.load) || + (this.latestHealth.load && (Array.isArray(this.latestHealth.load) ? this.latestHealth.load[0] : this.latestHealth.load)) || '0.00'; + var load = typeof loadStr === 'string' ? loadStr.split(' ')[0] : loadStr; var stats = [ { label: _('CPU Usage'), value: snapshot.cpu.value.toFixed(1) + '%', icon: 'โšก' }, @@ -193,7 +205,7 @@ return view.extend({ this.drawChart('cpu', this.cpuHistory, '#6366f1'); this.drawChart('memory', this.memoryHistory, '#22c55e'); this.drawChart('disk', this.diskHistory, '#f59e0b'); - this.drawNetworkChart(); + this.drawLoadChart(); }, drawChart: function(type, data, color) { @@ -282,6 +294,42 @@ return view.extend({ } }, + drawLoadChart: function() { + var svg = document.getElementById('chart-load'); + var currentEl = document.getElementById('current-load'); + if (!svg || this.loadHistory.length === 0) + return; + + var width = 600; + var height = 200; + var padding = 12; + + var values = this.loadHistory.map(function(d) { return d.value; }); + var maxValue = Math.max(100, Math.max.apply(Math, values)); + var minValue = 0; + var pathPoints = this.loadHistory.map(function(point, idx) { + var x = padding + (width - 2 * padding) * (idx / Math.max(1, this.maxDataPoints - 1)); + var y = height - padding - ((point.value - minValue) / (maxValue - minValue)) * (height - 2 * padding); + return x + ',' + y; + }, this).join(' '); + + svg.innerHTML = ''; + + svg.appendChild(E('polyline', { + 'points': pathPoints, + 'fill': 'none', + 'stroke': '#ec4899', + 'stroke-width': '2', + 'stroke-linejoin': 'round', + 'stroke-linecap': 'round' + })); + + if (currentEl) { + var lastPoint = this.loadHistory[this.loadHistory.length - 1]; + currentEl.textContent = (lastPoint.raw || '0.00'); + } + }, + updateCurrentStats: function() { var statsContainer = document.getElementById('current-stats'); if (statsContainer) diff --git a/package/secubox/luci-app-ndpid/Makefile b/package/secubox/luci-app-ndpid/Makefile index 821a5a15..2d0e2650 100644 --- a/package/secubox/luci-app-ndpid/Makefile +++ b/package/secubox/luci-app-ndpid/Makefile @@ -7,8 +7,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-ndpid -PKG_VERSION:=0.9.1 -PKG_RELEASE:=2 +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 PKG_ARCH:=all PKG_LICENSE:=Apache-2.0 diff --git a/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/ndpid/api.js b/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/ndpid/api.js index b7dd5497..c4333844 100644 --- a/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/ndpid/api.js +++ b/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/ndpid/api.js @@ -20,6 +20,18 @@ var callRealtimeFlows = rpc.declare({ expect: { } }); +var callDetailedFlows = rpc.declare({ + object: 'luci.ndpid', + method: 'get_detailed_flows', + expect: { flows: [] } +}); + +var callCategories = rpc.declare({ + object: 'luci.ndpid', + method: 'get_categories', + expect: { categories: [] } +}); + var callInterfaceStats = rpc.declare({ object: 'luci.ndpid', method: 'get_interface_stats', @@ -136,9 +148,11 @@ return baseclass.extend({ // Read methods getServiceStatus: callServiceStatus, getRealtimeFlows: callRealtimeFlows, + getDetailedFlows: callDetailedFlows, getInterfaceStats: callInterfaceStats, getTopApplications: callTopApplications, getTopProtocols: callTopProtocols, + getCategories: callCategories, getConfig: callConfig, getDashboard: callDashboard, getInterfaces: callInterfaces, diff --git a/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/ndpid/dashboard.css b/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/ndpid/dashboard.css index 607e0cda..82ed7b7f 100644 --- a/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/ndpid/dashboard.css +++ b/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/ndpid/dashboard.css @@ -527,3 +527,276 @@ background: var(--ndpi-border); border-radius: 4px; } + +/* Grid Layouts */ +.ndpi-grid-2 { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + gap: 20px; + margin-bottom: 20px; +} + +.ndpi-card-wide { + grid-column: 1 / -1; +} + +/* Applications List */ +.ndpi-apps-list { + display: flex; + flex-direction: column; + gap: 14px; +} + +.ndpi-app-item { + padding: 12px 0; + border-bottom: 1px solid var(--ndpi-border); +} + +.ndpi-app-item:last-child { + border-bottom: none; +} + +.ndpi-app-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 8px; +} + +.ndpi-app-icon { + font-size: 20px; + width: 28px; + text-align: center; +} + +.ndpi-app-info { + display: flex; + flex-direction: column; + flex: 1; + min-width: 0; +} + +.ndpi-app-name { + font-weight: 600; + color: var(--ndpi-text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.ndpi-app-host { + font-size: 11px; + color: var(--ndpi-text-muted); + font-family: var(--ndpi-font-mono); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.ndpi-app-bytes { + font-family: var(--ndpi-font-mono); + font-size: 12px; + font-weight: 600; + color: var(--ndpi-accent-cyan); + margin-left: auto; +} + +.ndpi-app-bar { + height: 6px; + background: var(--ndpi-bg-primary); + border-radius: 3px; + overflow: hidden; +} + +.ndpi-app-bar-fill { + height: 100%; + border-radius: 3px; + transition: width 0.5s ease; +} + +.ndpi-app-meta { + font-size: 10px; + color: var(--ndpi-text-muted); + margin-top: 6px; +} + +/* Categories List */ +.ndpi-categories-list { + display: flex; + flex-direction: column; + gap: 14px; +} + +.ndpi-category-item { + padding: 10px 0; + border-bottom: 1px solid var(--ndpi-border); +} + +.ndpi-category-item:last-child { + border-bottom: none; +} + +.ndpi-category-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.ndpi-category-name { + font-weight: 600; + font-size: 14px; +} + +.ndpi-category-bytes { + font-family: var(--ndpi-font-mono); + font-size: 12px; + color: var(--ndpi-text-secondary); +} + +.ndpi-category-bar { + height: 6px; + background: var(--ndpi-bg-primary); + border-radius: 3px; + overflow: hidden; +} + +.ndpi-category-bar-fill { + height: 100%; + border-radius: 3px; + transition: width 0.5s ease; +} + +.ndpi-category-meta { + font-size: 10px; + color: var(--ndpi-text-muted); + margin-top: 6px; +} + +/* Flows Table */ +.ndpi-flows-table-container { + max-height: 500px; + overflow-y: auto; +} + +.ndpi-flows-table { + width: 100%; + border-collapse: collapse; + font-size: 12px; +} + +.ndpi-flows-table th { + text-align: left; + padding: 10px 12px; + background: var(--ndpi-bg-tertiary); + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + color: var(--ndpi-text-muted); + border-bottom: 1px solid var(--ndpi-border); + position: sticky; + top: 0; + z-index: 1; +} + +.ndpi-flow-row { + transition: background 0.2s; +} + +.ndpi-flow-row:hover { + background: rgba(6, 182, 212, 0.05); +} + +.ndpi-flow-row td { + padding: 10px 12px; + border-bottom: 1px solid var(--ndpi-border); + vertical-align: middle; +} + +.ndpi-flow-ended { + opacity: 0.5; +} + +.ndpi-flow-app { + display: flex; + align-items: center; + gap: 8px; + min-width: 150px; +} + +.ndpi-flow-src, .ndpi-flow-dst { + font-family: var(--ndpi-font-mono); + font-size: 11px; + white-space: nowrap; +} + +.ndpi-flow-arrow { + color: var(--ndpi-text-muted); + font-size: 14px; +} + +/* Protocol Badges */ +.ndpi-proto-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 4px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; +} + +.ndpi-proto-tcp { + background: rgba(14, 165, 233, 0.2); + color: #0ea5e9; +} + +.ndpi-proto-udp { + background: rgba(16, 185, 129, 0.2); + color: #10b981; +} + +.ndpi-proto-icmp { + background: rgba(245, 158, 11, 0.2); + color: #f59e0b; +} + +/* Category Badge */ +.ndpi-category-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 4px; + font-size: 10px; + font-weight: 500; + color: white; + white-space: nowrap; +} + +/* Flow State */ +.ndpi-state-active { + color: var(--ndpi-accent-green); + font-size: 14px; +} + +.ndpi-state-ended { + color: var(--ndpi-text-muted); + font-size: 14px; +} + +.ndpi-empty-row { + text-align: center; + padding: 40px 20px; + color: var(--ndpi-text-muted); +} + +.ndpi-empty-hint { + font-size: 12px; + color: var(--ndpi-text-muted); + margin-top: 8px; +} + +/* Responsive for grids */ +@media (max-width: 900px) { + .ndpi-grid-2 { + grid-template-columns: 1fr; + } +} diff --git a/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/view/ndpid/dashboard.js b/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/view/ndpid/dashboard.js index e35783ba..112db59e 100644 --- a/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/view/ndpid/dashboard.js +++ b/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/view/ndpid/dashboard.js @@ -11,7 +11,56 @@ return view.extend({ pollActive: true, load: function() { - return api.getAllData(); + return Promise.all([ + api.getAllData(), + api.getCategories().catch(function() { return { categories: [] }; }) + ]).then(function(results) { + var data = results[0]; + data.categories = results[1]; + return data; + }); + }, + + getAppIcon: function(app, category) { + var icons = { + 'HTTP': '๐ŸŒ', 'HTTPS': '๐Ÿ”’', 'TLS': '๐Ÿ”’', 'SSL': '๐Ÿ”’', + 'DNS': '๐Ÿ“ก', 'NTP': '๐Ÿ•', 'DHCP': '๐Ÿ“‹', + 'SSH': '๐Ÿ–ฅ๏ธ', 'Telnet': '๐Ÿ’ป', + 'YouTube': 'โ–ถ๏ธ', 'Netflix': '๐ŸŽฌ', 'Twitch': '๐ŸŽฎ', + 'Facebook': '๐Ÿ‘ค', 'Twitter': '๐Ÿฆ', 'Instagram': '๐Ÿ“ท', 'TikTok': '๐ŸŽต', + 'WhatsApp': '๐Ÿ’ฌ', 'Telegram': 'โœˆ๏ธ', 'Discord': '๐ŸŽง', + 'BitTorrent': '๐Ÿ“ฅ', 'eDonkey': '๐Ÿ“ฅ', + 'Spotify': '๐ŸŽต', 'AppleMusic': '๐ŸŽต', + 'Dropbox': '๐Ÿ“ฆ', 'GoogleDrive': '๐Ÿ“ฆ', 'OneDrive': '๐Ÿ“ฆ', + 'Zoom': '๐Ÿ“น', 'Teams': '๐Ÿ‘ฅ', 'Skype': '๐Ÿ“ž', + 'VPN': '๐Ÿ›ก๏ธ', 'OpenVPN': '๐Ÿ›ก๏ธ', 'WireGuard': '๐Ÿ›ก๏ธ', + 'QUIC': 'โšก', 'HTTP2': 'โšก', + 'SMTP': '๐Ÿ“ง', 'IMAP': '๐Ÿ“ง', 'POP3': '๐Ÿ“ง', + 'FTP': '๐Ÿ“', 'SFTP': '๐Ÿ“', 'SMB': '๐Ÿ“', + 'ICMP': '๐Ÿ“ถ', 'IGMP': '๐Ÿ“ก', + 'Unknown': 'โ“' + }; + return icons[app] || icons[category] || '๐Ÿ“ฆ'; + }, + + getCategoryColor: function(category) { + var colors = { + 'Web': '#3b82f6', + 'Video': '#ef4444', + 'Streaming': '#f59e0b', + 'SocialNetwork': '#ec4899', + 'Chat': '#8b5cf6', + 'VoIP': '#10b981', + 'Game': '#06b6d4', + 'Download': '#f97316', + 'Cloud': '#6366f1', + 'VPN': '#14b8a6', + 'Mail': '#84cc16', + 'FileTransfer': '#a855f7', + 'Network': '#64748b', + 'Unknown': '#94a3b8' + }; + return colors[category] || '#64748b'; }, updateDashboard: function(data) { @@ -46,7 +95,7 @@ return view.extend({ }); // Update interface stats - var interfaces = (data.interfaces || {}).interfaces || []; + var interfaces = Array.isArray(data.interfaces) ? data.interfaces : (data.interfaces || {}).interfaces || []; interfaces.forEach(function(iface) { var card = document.querySelector('.ndpi-iface-card[data-iface="' + iface.name + '"]'); if (!card) return; @@ -121,9 +170,11 @@ return view.extend({ var service = dashboard.service || {}; var flows = dashboard.flows || {}; var system = dashboard.system || {}; - var interfaces = (data.interfaces || {}).interfaces || []; - var applications = (data.applications || {}).applications || []; - var protocols = (data.protocols || {}).protocols || []; + // Handle both array and object formats from API + var interfaces = Array.isArray(data.interfaces) ? data.interfaces : (data.interfaces || {}).interfaces || []; + var applications = Array.isArray(data.applications) ? data.applications : (data.applications || {}).applications || []; + var protocols = Array.isArray(data.protocols) ? data.protocols : (data.protocols || {}).protocols || []; + var categories = Array.isArray(data.categories) ? data.categories : (data.categories || {}).categories || []; var view = E('div', { 'class': 'ndpid-dashboard' }, [ E('link', { 'rel': 'stylesheet', 'href': L.resource('ndpid/dashboard.css') }), @@ -276,43 +327,81 @@ return view.extend({ ) ]), - // Top Applications - E('div', { 'class': 'ndpi-card' }, [ - E('div', { 'class': 'ndpi-card-header' }, [ - E('div', { 'class': 'ndpi-card-title' }, [ - E('span', { 'class': 'ndpi-card-title-icon' }, '๐Ÿ“ฑ'), - 'Top Applications' - ]) - ]), - E('div', { 'class': 'ndpi-card-body' }, - applications.length > 0 ? - E('div', { 'class': 'ndpi-table-container' }, [ - E('table', { 'class': 'ndpi-table' }, [ - E('thead', {}, [ - E('tr', {}, [ - E('th', {}, 'Application'), - E('th', {}, 'Flows'), - E('th', {}, 'Traffic') - ]) - ]), - E('tbody', {}, - applications.map(function(app) { - return E('tr', {}, [ - E('td', {}, [ - E('span', { 'class': 'ndpi-app-name' }, app.name || 'unknown') + // Grid layout for Applications and Categories + E('div', { 'class': 'ndpi-grid-2' }, [ + // Top Applications + E('div', { 'class': 'ndpi-card' }, [ + E('div', { 'class': 'ndpi-card-header' }, [ + E('div', { 'class': 'ndpi-card-title' }, [ + E('span', { 'class': 'ndpi-card-title-icon' }, '๐Ÿ“ฑ'), + 'Top Applications' + ]), + E('div', { 'class': 'ndpi-card-badge' }, applications.length + ' detected') + ]), + E('div', { 'class': 'ndpi-card-body' }, + applications.length > 0 ? + E('div', { 'class': 'ndpi-apps-list' }, + (function() { + var maxBytes = Math.max.apply(null, applications.map(function(a) { return a.bytes || 0; })) || 1; + return applications.slice(0, 8).map(function(app) { + var pct = Math.round(((app.bytes || 0) / maxBytes) * 100); + return E('div', { 'class': 'ndpi-app-item' }, [ + E('div', { 'class': 'ndpi-app-header' }, [ + E('span', { 'class': 'ndpi-app-icon' }, self.getAppIcon(app.name, app.category)), + E('span', { 'class': 'ndpi-app-name' }, app.name || 'Unknown'), + E('span', { 'class': 'ndpi-app-bytes' }, api.formatBytes(app.bytes || 0)) ]), - E('td', { 'class': 'mono' }, api.formatNumber(app.flows)), - E('td', { 'class': 'mono' }, api.formatBytes(app.bytes)) + E('div', { 'class': 'ndpi-app-bar' }, [ + E('div', { 'class': 'ndpi-app-bar-fill', 'style': 'width:' + pct + '%;background:' + self.getCategoryColor(app.category) }) + ]), + E('div', { 'class': 'ndpi-app-meta' }, (app.flows || 0) + ' flows ยท ' + (app.category || 'Unknown')) ]); - }) - ) + }); + })() + ) : + E('div', { 'class': 'ndpi-empty' }, [ + E('div', { 'class': 'ndpi-empty-icon' }, '๐Ÿ“ฑ'), + E('div', { 'class': 'ndpi-empty-text' }, 'No applications detected yet'), + E('p', {}, 'Generate network traffic to see app detection') ]) - ]) : - E('div', { 'class': 'ndpi-empty' }, [ - E('div', { 'class': 'ndpi-empty-icon' }, '๐Ÿ“ฑ'), - E('div', { 'class': 'ndpi-empty-text' }, 'No applications detected yet') - ]) - ) + ) + ]), + + // Traffic Categories + E('div', { 'class': 'ndpi-card' }, [ + E('div', { 'class': 'ndpi-card-header' }, [ + E('div', { 'class': 'ndpi-card-title' }, [ + E('span', { 'class': 'ndpi-card-title-icon' }, '๐Ÿท๏ธ'), + 'Traffic Categories' + ]), + E('div', { 'class': 'ndpi-card-badge' }, categories.length + ' types') + ]), + E('div', { 'class': 'ndpi-card-body' }, + categories.length > 0 ? + E('div', { 'class': 'ndpi-categories-list' }, + (function() { + var maxBytes = Math.max.apply(null, categories.map(function(c) { return c.bytes || 0; })) || 1; + return categories.slice(0, 8).map(function(cat) { + var pct = Math.round(((cat.bytes || 0) / maxBytes) * 100); + return E('div', { 'class': 'ndpi-category-item' }, [ + E('div', { 'class': 'ndpi-category-header' }, [ + E('span', { 'class': 'ndpi-category-name', 'style': 'color:' + self.getCategoryColor(cat.name) }, cat.name), + E('span', { 'class': 'ndpi-category-bytes' }, api.formatBytes(cat.bytes || 0)) + ]), + E('div', { 'class': 'ndpi-category-bar' }, [ + E('div', { 'class': 'ndpi-category-bar-fill', 'style': 'width:' + pct + '%;background:' + self.getCategoryColor(cat.name) }) + ]), + E('div', { 'class': 'ndpi-category-meta' }, (cat.apps || 0) + ' apps ยท ' + (cat.flows || 0) + ' flows') + ]); + }); + })() + ) : + E('div', { 'class': 'ndpi-empty' }, [ + E('div', { 'class': 'ndpi-empty-icon' }, '๐Ÿท๏ธ'), + E('div', { 'class': 'ndpi-empty-text' }, 'No categories detected yet') + ]) + ) + ]) ]), // Top Protocols diff --git a/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/view/ndpid/flows.js b/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/view/ndpid/flows.js index 2779986e..5c33a52d 100644 --- a/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/view/ndpid/flows.js +++ b/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/view/ndpid/flows.js @@ -12,27 +12,85 @@ return view.extend({ load: function() { return Promise.all([ - api.getRealtimeFlows(), - api.getInterfaceStats(), - api.getTopProtocols() + api.getRealtimeFlows().catch(function(e) { console.log('getRealtimeFlows error:', e); return {}; }), + api.getDetailedFlows().catch(function(e) { console.log('getDetailedFlows error:', e); return { flows: [] }; }), + api.getTopApplications().catch(function(e) { console.log('getTopApplications error:', e); return { applications: [] }; }), + api.getCategories().catch(function(e) { console.log('getCategories error:', e); return { categories: [] }; }), + api.getInterfaceStats().catch(function(e) { console.log('getInterfaceStats error:', e); return { interfaces: [] }; }), + api.getTopProtocols().catch(function(e) { console.log('getTopProtocols error:', e); return { protocols: [] }; }) ]).then(function(results) { + console.log('nDPId flows.js load results:', results); + console.log('Detailed flows:', results[1]); + console.log('Applications:', results[2]); + console.log('Categories:', results[3]); return { - flows: results[0], - interfaces: results[1], - protocols: results[2] + status: results[0], + flows: results[1], + applications: results[2], + categories: results[3], + interfaces: results[4], + protocols: results[5] }; }); }, - updateFlows: function(data) { - var flows = data.flows || {}; + getProtoName: function(proto) { + var protos = { '6': 'TCP', '17': 'UDP', '1': 'ICMP', 'tcp': 'TCP', 'udp': 'UDP', 'icmp': 'ICMP' }; + return protos[proto] || proto || '?'; + }, + + getAppIcon: function(app, category) { + var icons = { + 'HTTP': '๐ŸŒ', 'HTTPS': '๐Ÿ”’', 'TLS': '๐Ÿ”’', 'SSL': '๐Ÿ”’', + 'DNS': '๐Ÿ“ก', 'NTP': '๐Ÿ•', 'DHCP': '๐Ÿ“‹', + 'SSH': '๐Ÿ–ฅ๏ธ', 'Telnet': '๐Ÿ’ป', + 'YouTube': 'โ–ถ๏ธ', 'Netflix': '๐ŸŽฌ', 'Twitch': '๐ŸŽฎ', + 'Facebook': '๐Ÿ‘ค', 'Twitter': '๐Ÿฆ', 'Instagram': '๐Ÿ“ท', 'TikTok': '๐ŸŽต', + 'WhatsApp': '๐Ÿ’ฌ', 'Telegram': 'โœˆ๏ธ', 'Discord': '๐ŸŽง', + 'BitTorrent': '๐Ÿ“ฅ', 'eDonkey': '๐Ÿ“ฅ', + 'Spotify': '๐ŸŽต', 'AppleMusic': '๐ŸŽต', + 'Dropbox': '๐Ÿ“ฆ', 'GoogleDrive': '๐Ÿ“ฆ', 'OneDrive': '๐Ÿ“ฆ', + 'Zoom': '๐Ÿ“น', 'Teams': '๐Ÿ‘ฅ', 'Skype': '๐Ÿ“ž', + 'VPN': '๐Ÿ›ก๏ธ', 'OpenVPN': '๐Ÿ›ก๏ธ', 'WireGuard': '๐Ÿ›ก๏ธ', + 'QUIC': 'โšก', 'HTTP2': 'โšก', + 'SMTP': '๐Ÿ“ง', 'IMAP': '๐Ÿ“ง', 'POP3': '๐Ÿ“ง', + 'FTP': '๐Ÿ“', 'SFTP': '๐Ÿ“', 'SMB': '๐Ÿ“', + 'ICMP': '๐Ÿ“ถ', 'IGMP': '๐Ÿ“ก', + 'Unknown': 'โ“' + }; + return icons[app] || icons[category] || '๐Ÿ“ฆ'; + }, + + getCategoryColor: function(category) { + var colors = { + 'Web': '#3b82f6', + 'Video': '#ef4444', + 'Streaming': '#f59e0b', + 'SocialNetwork': '#ec4899', + 'Chat': '#8b5cf6', + 'VoIP': '#10b981', + 'Game': '#06b6d4', + 'Download': '#f97316', + 'Cloud': '#6366f1', + 'VPN': '#14b8a6', + 'Mail': '#84cc16', + 'FileTransfer': '#a855f7', + 'Network': '#64748b', + 'Unknown': '#94a3b8' + }; + return colors[category] || '#64748b'; + }, + + updateData: function(data) { + var self = this; // Update flow counts + var status = data.status || {}; var activeEl = document.querySelector('.ndpi-flows-active'); var totalEl = document.querySelector('.ndpi-flows-total'); if (activeEl) { - var newActive = api.formatNumber(flows.flows_active || 0); + var newActive = api.formatNumber(status.flows_active || 0); if (activeEl.textContent !== newActive) { activeEl.textContent = newActive; activeEl.classList.add('ndpi-value-updated'); @@ -41,23 +99,61 @@ return view.extend({ } if (totalEl) { - var newTotal = api.formatNumber(flows.flow_count || 0); - if (totalEl.textContent !== newTotal) { - totalEl.textContent = newTotal; - } + totalEl.textContent = api.formatNumber(status.flow_count || 0); } - // Update interface stats - var interfaces = (data.interfaces || {}).interfaces || []; - interfaces.forEach(function(iface) { - var row = document.querySelector('.ndpi-iface-row[data-iface="' + iface.name + '"]'); - if (!row) return; + // Update flows table + var flows = Array.isArray(data.flows) ? data.flows : (data.flows || {}).flows || []; + var tbody = document.querySelector('.ndpi-flows-tbody'); + if (tbody && flows.length > 0) { + tbody.innerHTML = ''; + flows.slice(0, 50).forEach(function(flow) { + var tr = document.createElement('tr'); + tr.className = 'ndpi-flow-row ' + (flow.state === 'ended' ? 'ndpi-flow-ended' : 'ndpi-flow-active'); + tr.innerHTML = [ + '', + '' + self.getAppIcon(flow.app, flow.category) + '', + '
', + '' + (flow.app || 'Unknown') + '', + '' + (flow.hostname || '') + '', + '
', + '', + '' + flow.src_ip + ':' + flow.src_port + '', + 'โ†’', + '' + flow.dst_ip + ':' + flow.dst_port + '', + '' + self.getProtoName(flow.proto) + '', + '' + (flow.category || 'Unknown') + '', + '' + api.formatBytes((flow.bytes_rx || 0) + (flow.bytes_tx || 0)) + '', + '' + (flow.state === 'active' ? 'โ—' : 'โ—‹') + '' + ].join(''); + tbody.appendChild(tr); + }); + } else if (tbody) { + tbody.innerHTML = 'No flows detected yet'; + } - row.querySelector('.ndpi-iface-tcp').textContent = api.formatNumber(iface.tcp); - row.querySelector('.ndpi-iface-udp').textContent = api.formatNumber(iface.udp); - row.querySelector('.ndpi-iface-icmp').textContent = api.formatNumber(iface.icmp); - row.querySelector('.ndpi-iface-bytes').textContent = api.formatBytes(iface.ip_bytes); - }); + // Update top applications + var apps = Array.isArray(data.applications) ? data.applications : (data.applications || {}).applications || []; + var appsContainer = document.querySelector('.ndpi-apps-list'); + if (appsContainer && apps.length > 0) { + var maxBytes = Math.max.apply(null, apps.map(function(a) { return a.bytes || 0; })) || 1; + appsContainer.innerHTML = ''; + apps.slice(0, 10).forEach(function(app) { + var pct = Math.round(((app.bytes || 0) / maxBytes) * 100); + var div = document.createElement('div'); + div.className = 'ndpi-app-item'; + div.innerHTML = [ + '
', + '' + self.getAppIcon(app.name, app.category) + '', + '' + app.name + '', + '' + api.formatBytes(app.bytes || 0) + '', + '
', + '
', + '
' + (app.flows || 0) + ' flows ยท ' + (app.category || 'Unknown') + '
' + ].join(''); + appsContainer.appendChild(div); + }); + } }, startPolling: function() { @@ -68,12 +164,14 @@ return view.extend({ if (!this.pollActive) return Promise.resolve(); return Promise.all([ - api.getRealtimeFlows(), - api.getInterfaceStats() + api.getRealtimeFlows().catch(function() { return {}; }), + api.getDetailedFlows().catch(function() { return { flows: [] }; }), + api.getTopApplications().catch(function() { return { applications: [] }; }) ]).then(L.bind(function(results) { - this.updateFlows({ - flows: results[0], - interfaces: results[1] + this.updateData({ + status: results[0], + flows: results[1], + applications: results[2] }); }, this)); }, this), this.pollInterval); @@ -86,25 +184,37 @@ return view.extend({ render: function(data) { var self = this; - var flows = data.flows || {}; - var interfaces = (data.interfaces || {}).interfaces || []; - var protocols = (data.protocols || {}).protocols || []; + var status = data.status || {}; + + // Debug: log raw data + console.log('RENDER - raw data.flows:', data.flows); + console.log('RENDER - Array.isArray(data.flows):', Array.isArray(data.flows)); + + // Handle both array and object formats from API + var flows = Array.isArray(data.flows) ? data.flows : (data.flows || {}).flows || []; + var applications = Array.isArray(data.applications) ? data.applications : (data.applications || {}).applications || []; + var categories = Array.isArray(data.categories) ? data.categories : (data.categories || {}).categories || []; + var interfaces = Array.isArray(data.interfaces) ? data.interfaces : (data.interfaces || {}).interfaces || []; + var protocols = Array.isArray(data.protocols) ? data.protocols : (data.protocols || {}).protocols || []; + + // Debug: log processed data + console.log('RENDER - processed flows.length:', flows.length); + console.log('RENDER - processed apps.length:', applications.length); - // Calculate protocol totals var totalPackets = protocols.reduce(function(sum, p) { return sum + (p.count || 0); }, 0); - var view = E('div', { 'class': 'ndpid-dashboard' }, [ + var view = E('div', { 'class': 'ndpid-dashboard ndpid-flows-page' }, [ E('link', { 'rel': 'stylesheet', 'href': L.resource('ndpid/dashboard.css') }), // Header E('div', { 'class': 'ndpi-header' }, [ E('div', { 'class': 'ndpi-logo' }, [ - E('div', { 'class': 'ndpi-logo-icon' }, '๐Ÿ“Š'), - E('div', { 'class': 'ndpi-logo-text' }, ['Flow ', E('span', {}, 'Statistics')]) + E('div', { 'class': 'ndpi-logo-icon' }, '๐Ÿ”'), + E('div', { 'class': 'ndpi-logo-text' }, ['Deep Packet ', E('span', {}, 'Inspection')]) ]) ]), - // Flow Summary + // Quick Stats E('div', { 'class': 'ndpi-quick-stats' }, [ E('div', { 'class': 'ndpi-quick-stat', 'style': '--stat-gradient: linear-gradient(135deg, #10b981, #34d399)' }, [ E('div', { 'class': 'ndpi-quick-stat-header' }, [ @@ -112,8 +222,8 @@ return view.extend({ E('span', { 'class': 'ndpi-quick-stat-label' }, 'Active Flows') ]), E('div', { 'class': 'ndpi-quick-stat-value ndpi-flows-active' }, - api.formatNumber(flows.flows_active || 0)), - E('div', { 'class': 'ndpi-quick-stat-sub' }, 'Currently tracked') + api.formatNumber(status.flows_active || 0)), + E('div', { 'class': 'ndpi-quick-stat-sub' }, 'Real-time tracking') ]), E('div', { 'class': 'ndpi-quick-stat' }, [ E('div', { 'class': 'ndpi-quick-stat-header' }, [ @@ -121,109 +231,197 @@ return view.extend({ E('span', { 'class': 'ndpi-quick-stat-label' }, 'Total Flows') ]), E('div', { 'class': 'ndpi-quick-stat-value ndpi-flows-total' }, - api.formatNumber(flows.flow_count || 0)), - E('div', { 'class': 'ndpi-quick-stat-sub' }, 'Since service start') + api.formatNumber(status.flow_count || 0)), + E('div', { 'class': 'ndpi-quick-stat-sub' }, 'Since start') ]), E('div', { 'class': 'ndpi-quick-stat' }, [ E('div', { 'class': 'ndpi-quick-stat-header' }, [ - E('span', { 'class': 'ndpi-quick-stat-icon' }, '๐Ÿ“ฆ'), - E('span', { 'class': 'ndpi-quick-stat-label' }, 'Total Packets') + E('span', { 'class': 'ndpi-quick-stat-icon' }, '๐Ÿ“ฑ'), + E('span', { 'class': 'ndpi-quick-stat-label' }, 'Applications') ]), E('div', { 'class': 'ndpi-quick-stat-value' }, - api.formatNumber(totalPackets)), - E('div', { 'class': 'ndpi-quick-stat-sub' }, 'TCP + UDP + ICMP') + api.formatNumber(applications.length)), + E('div', { 'class': 'ndpi-quick-stat-sub' }, 'Detected') ]), E('div', { 'class': 'ndpi-quick-stat' }, [ E('div', { 'class': 'ndpi-quick-stat-header' }, [ - E('span', { 'class': 'ndpi-quick-stat-icon' }, 'โฑ'), - E('span', { 'class': 'ndpi-quick-stat-label' }, 'Uptime') + E('span', { 'class': 'ndpi-quick-stat-icon' }, '๐Ÿท๏ธ'), + E('span', { 'class': 'ndpi-quick-stat-label' }, 'Categories') ]), E('div', { 'class': 'ndpi-quick-stat-value' }, - api.formatUptime(flows.uptime || 0)), - E('div', { 'class': 'ndpi-quick-stat-sub' }, 'Service runtime') + api.formatNumber(categories.length)), + E('div', { 'class': 'ndpi-quick-stat-sub' }, 'Traffic types') ]) ]), - // Interface Statistics Table - E('div', { 'class': 'ndpi-card' }, [ - E('div', { 'class': 'ndpi-card-header' }, [ - E('div', { 'class': 'ndpi-card-title' }, [ - E('span', { 'class': 'ndpi-card-title-icon' }, '๐ŸŒ'), - 'Per-Interface Statistics' + // Main content grid + E('div', { 'class': 'ndpi-grid-2' }, [ + // Flows Table + E('div', { 'class': 'ndpi-card ndpi-card-wide' }, [ + E('div', { 'class': 'ndpi-card-header' }, [ + E('div', { 'class': 'ndpi-card-title' }, [ + E('span', { 'class': 'ndpi-card-title-icon' }, '๐Ÿ”'), + 'Live Flow Detection' + ]), + E('div', { 'class': 'ndpi-card-badge' }, flows.length + ' flows') ]), - E('div', { 'class': 'ndpi-card-badge' }, - interfaces.length + ' interface' + (interfaces.length !== 1 ? 's' : '')) - ]), - E('div', { 'class': 'ndpi-card-body' }, - interfaces.length > 0 ? - E('div', { 'class': 'ndpi-table-container' }, [ - E('table', { 'class': 'ndpi-table' }, [ + E('div', { 'class': 'ndpi-card-body ndpi-flows-table-container' }, + flows.length > 0 ? + E('table', { 'class': 'ndpi-table ndpi-flows-table' }, [ E('thead', {}, [ E('tr', {}, [ - E('th', {}, 'Interface'), - E('th', {}, 'TCP'), - E('th', {}, 'UDP'), - E('th', {}, 'ICMP'), - E('th', {}, 'Total Bytes') + E('th', {}, 'Application'), + E('th', {}, 'Source'), + E('th', {}, ''), + E('th', {}, 'Destination'), + E('th', {}, 'Proto'), + E('th', {}, 'Category'), + E('th', {}, 'Traffic'), + E('th', {}, '') ]) ]), - E('tbody', {}, - interfaces.map(function(iface) { - return E('tr', { 'class': 'ndpi-iface-row', 'data-iface': iface.name }, [ - E('td', {}, [ - E('span', { 'class': 'ndpi-app-name' }, iface.name) + E('tbody', { 'class': 'ndpi-flows-tbody' }, + flows.slice(0, 50).map(function(flow) { + return E('tr', { 'class': 'ndpi-flow-row ' + (flow.state === 'ended' ? 'ndpi-flow-ended' : 'ndpi-flow-active') }, [ + E('td', { 'class': 'ndpi-flow-app' }, [ + E('span', { 'class': 'ndpi-app-icon' }, self.getAppIcon(flow.app, flow.category)), + E('div', { 'class': 'ndpi-app-info' }, [ + E('span', { 'class': 'ndpi-app-name' }, flow.app || 'Unknown'), + E('span', { 'class': 'ndpi-app-host' }, flow.hostname || '') + ]) ]), - E('td', { 'class': 'mono ndpi-iface-tcp' }, api.formatNumber(iface.tcp)), - E('td', { 'class': 'mono ndpi-iface-udp' }, api.formatNumber(iface.udp)), - E('td', { 'class': 'mono ndpi-iface-icmp' }, api.formatNumber(iface.icmp)), - E('td', { 'class': 'mono ndpi-iface-bytes' }, api.formatBytes(iface.ip_bytes)) + E('td', { 'class': 'ndpi-flow-src mono' }, flow.src_ip + ':' + flow.src_port), + E('td', { 'class': 'ndpi-flow-arrow' }, 'โ†’'), + E('td', { 'class': 'ndpi-flow-dst mono' }, flow.dst_ip + ':' + flow.dst_port), + E('td', { 'class': 'ndpi-flow-proto' }, [ + E('span', { 'class': 'ndpi-proto-badge ndpi-proto-' + self.getProtoName(flow.proto).toLowerCase() }, self.getProtoName(flow.proto)) + ]), + E('td', { 'class': 'ndpi-flow-category' }, [ + E('span', { 'class': 'ndpi-category-badge', 'style': 'background:' + self.getCategoryColor(flow.category) }, flow.category || 'Unknown') + ]), + E('td', { 'class': 'ndpi-flow-bytes mono' }, api.formatBytes((flow.bytes_rx || 0) + (flow.bytes_tx || 0))), + E('td', { 'class': 'ndpi-flow-state' }, [ + E('span', { 'class': 'ndpi-state-' + flow.state }, flow.state === 'active' ? 'โ—' : 'โ—‹') + ]) ]); }) ) + ]) : + E('div', { 'class': 'ndpi-empty' }, [ + E('div', { 'class': 'ndpi-empty-icon' }, '๐Ÿ”'), + E('div', { 'class': 'ndpi-empty-text' }, 'No flows detected yet'), + E('div', { 'class': 'ndpi-empty-hint' }, 'Generate some network traffic to see detection') ]) - ]) : - E('div', { 'class': 'ndpi-empty' }, [ - E('div', { 'class': 'ndpi-empty-icon' }, '๐Ÿ“Š'), - E('div', { 'class': 'ndpi-empty-text' }, 'No interface statistics available') - ]) - ) + ) + ]), + + // Top Applications + E('div', { 'class': 'ndpi-card' }, [ + E('div', { 'class': 'ndpi-card-header' }, [ + E('div', { 'class': 'ndpi-card-title' }, [ + E('span', { 'class': 'ndpi-card-title-icon' }, '๐Ÿ“ฑ'), + 'Top Applications' + ]) + ]), + E('div', { 'class': 'ndpi-card-body' }, + applications.length > 0 ? + E('div', { 'class': 'ndpi-apps-list' }, + (function() { + var maxBytes = Math.max.apply(null, applications.map(function(a) { return a.bytes || 0; })) || 1; + return applications.slice(0, 10).map(function(app) { + var pct = Math.round(((app.bytes || 0) / maxBytes) * 100); + return E('div', { 'class': 'ndpi-app-item' }, [ + E('div', { 'class': 'ndpi-app-header' }, [ + E('span', { 'class': 'ndpi-app-icon' }, self.getAppIcon(app.name, app.category)), + E('span', { 'class': 'ndpi-app-name' }, app.name), + E('span', { 'class': 'ndpi-app-bytes' }, api.formatBytes(app.bytes || 0)) + ]), + E('div', { 'class': 'ndpi-app-bar' }, [ + E('div', { 'class': 'ndpi-app-bar-fill', 'style': 'width:' + pct + '%;background:' + self.getCategoryColor(app.category) }) + ]), + E('div', { 'class': 'ndpi-app-meta' }, (app.flows || 0) + ' flows ยท ' + (app.category || 'Unknown')) + ]); + }); + })() + ) : + E('div', { 'class': 'ndpi-empty' }, [ + E('div', { 'class': 'ndpi-empty-icon' }, '๐Ÿ“ฑ'), + E('div', { 'class': 'ndpi-empty-text' }, 'No applications detected yet') + ]) + ) + ]) ]), - // Protocol Breakdown - E('div', { 'class': 'ndpi-card' }, [ - E('div', { 'class': 'ndpi-card-header' }, [ - E('div', { 'class': 'ndpi-card-title' }, [ - E('span', { 'class': 'ndpi-card-title-icon' }, '๐Ÿ“ก'), - 'Protocol Breakdown' - ]) + // Protocol & Category breakdown + E('div', { 'class': 'ndpi-grid-2' }, [ + // Protocol Distribution + E('div', { 'class': 'ndpi-card' }, [ + E('div', { 'class': 'ndpi-card-header' }, [ + E('div', { 'class': 'ndpi-card-title' }, [ + E('span', { 'class': 'ndpi-card-title-icon' }, '๐Ÿ“ก'), + 'Protocol Distribution' + ]) + ]), + E('div', { 'class': 'ndpi-card-body' }, + protocols.length > 0 ? + E('div', { 'class': 'ndpi-protocol-grid' }, + protocols.map(function(proto) { + var pct = totalPackets > 0 ? Math.round((proto.count / totalPackets) * 100) : 0; + var color = proto.name === 'TCP' ? '#0ea5e9' : + proto.name === 'UDP' ? '#10b981' : '#f59e0b'; + return E('div', { 'class': 'ndpi-protocol-item' }, [ + E('div', { 'class': 'ndpi-protocol-header' }, [ + E('span', { 'class': 'ndpi-protocol-name' }, proto.name), + E('span', { 'class': 'ndpi-protocol-count' }, api.formatNumber(proto.count)) + ]), + E('div', { 'class': 'ndpi-protocol-bar' }, [ + E('div', { 'class': 'ndpi-protocol-bar-fill', 'style': 'width:' + pct + '%;background:' + color }) + ]), + E('div', { 'class': 'ndpi-protocol-pct' }, pct + '%') + ]); + }) + ) : + E('div', { 'class': 'ndpi-empty' }, [ + E('div', { 'class': 'ndpi-empty-icon' }, '๐Ÿ“ก'), + E('div', { 'class': 'ndpi-empty-text' }, 'No protocol data') + ]) + ) ]), - E('div', { 'class': 'ndpi-card-body' }, - protocols.length > 0 ? - E('div', { 'class': 'ndpi-protocol-grid' }, - protocols.map(function(proto) { - var pct = totalPackets > 0 ? Math.round((proto.count / totalPackets) * 100) : 0; - var color = proto.name === 'TCP' ? '#0ea5e9' : - proto.name === 'UDP' ? '#10b981' : '#f59e0b'; - return E('div', { 'class': 'ndpi-protocol-item' }, [ - E('div', { 'class': 'ndpi-protocol-header' }, [ - E('span', { 'class': 'ndpi-protocol-name' }, proto.name), - E('span', { 'class': 'ndpi-protocol-count' }, api.formatNumber(proto.count)) - ]), - E('div', { 'class': 'ndpi-protocol-bar' }, [ - E('div', { - 'class': 'ndpi-protocol-bar-fill', - 'style': 'width: ' + pct + '%; background: ' + color - }) - ]), - E('div', { 'class': 'ndpi-protocol-pct' }, pct + '%') - ]); - }) - ) : - E('div', { 'class': 'ndpi-empty' }, [ - E('div', { 'class': 'ndpi-empty-icon' }, '๐Ÿ“ก'), - E('div', { 'class': 'ndpi-empty-text' }, 'No protocol data available') - ]) - ) + + // Categories + E('div', { 'class': 'ndpi-card' }, [ + E('div', { 'class': 'ndpi-card-header' }, [ + E('div', { 'class': 'ndpi-card-title' }, [ + E('span', { 'class': 'ndpi-card-title-icon' }, '๐Ÿท๏ธ'), + 'Traffic Categories' + ]) + ]), + E('div', { 'class': 'ndpi-card-body' }, + categories.length > 0 ? + E('div', { 'class': 'ndpi-categories-list' }, + (function() { + var maxBytes = Math.max.apply(null, categories.map(function(c) { return c.bytes || 0; })) || 1; + return categories.slice(0, 8).map(function(cat) { + var pct = Math.round(((cat.bytes || 0) / maxBytes) * 100); + return E('div', { 'class': 'ndpi-category-item' }, [ + E('div', { 'class': 'ndpi-category-header' }, [ + E('span', { 'class': 'ndpi-category-name', 'style': 'color:' + self.getCategoryColor(cat.name) }, cat.name), + E('span', { 'class': 'ndpi-category-bytes' }, api.formatBytes(cat.bytes || 0)) + ]), + E('div', { 'class': 'ndpi-category-bar' }, [ + E('div', { 'class': 'ndpi-category-bar-fill', 'style': 'width:' + pct + '%;background:' + self.getCategoryColor(cat.name) }) + ]), + E('div', { 'class': 'ndpi-category-meta' }, (cat.apps || 0) + ' apps ยท ' + (cat.flows || 0) + ' flows') + ]); + }); + })() + ) : + E('div', { 'class': 'ndpi-empty' }, [ + E('div', { 'class': 'ndpi-empty-icon' }, '๐Ÿท๏ธ'), + E('div', { 'class': 'ndpi-empty-text' }, 'No categories detected') + ]) + ) + ]) ]) ]); diff --git a/package/secubox/luci-app-ndpid/root/usr/bin/ndpid-collector b/package/secubox/luci-app-ndpid/root/usr/bin/ndpid-collector old mode 100644 new mode 100755 diff --git a/package/secubox/luci-app-ndpid/root/usr/bin/ndpid-compat b/package/secubox/luci-app-ndpid/root/usr/bin/ndpid-compat index 636d8002..ed7297f5 100644 --- a/package/secubox/luci-app-ndpid/root/usr/bin/ndpid-compat +++ b/package/secubox/luci-app-ndpid/root/usr/bin/ndpid-compat @@ -1,6 +1,6 @@ #!/bin/sh # nDPId to Netifyd Compatibility Layer -# Translates nDPId events to Netifyd-compatible format +# Translates nDPId events to Netifyd-compatible format with enhanced detection # Copyright (C) 2025 CyberMind.fr . /lib/functions.sh @@ -10,16 +10,19 @@ DISTRIBUTOR_SOCK="/var/run/ndpid/distributor.sock" STATUS_FILE="/var/run/netifyd/status.json" FLOWS_FILE="/tmp/ndpid-flows.json" +APPS_FILE="/tmp/ndpid-apps.json" STATS_FILE="/tmp/ndpid-stats.json" -STATS_HISTORY="/tmp/ndpid-stats-history.json" UPDATE_INTERVAL=1 -MAX_HISTORY=1440 +MAX_FLOWS=500 +MAX_APPS=100 -# State variables (stored in temp files for shell compatibility) +# State directory STATE_DIR="/tmp/ndpid-state" FLOWS_ACTIVE_FILE="$STATE_DIR/flows_active" FLOW_COUNT_FILE="$STATE_DIR/flow_count" STATS_FILE_TMP="$STATE_DIR/stats" +FLOWS_TMP="$STATE_DIR/flows" +APPS_TMP="$STATE_DIR/apps" # Initialize state init_state() { @@ -28,16 +31,17 @@ init_state() { echo "0" > "$FLOWS_ACTIVE_FILE" echo "0" > "$FLOW_COUNT_FILE" echo "{}" > "$STATS_FILE_TMP" + echo "[]" > "$FLOWS_TMP" + echo "{}" > "$APPS_TMP" } -# Increment counter +# Counter operations inc_counter() { local file="$1" local val=$(cat "$file" 2>/dev/null || echo 0) echo $((val + 1)) > "$file" } -# Decrement counter dec_counter() { local file="$1" local val=$(cat "$file" 2>/dev/null || echo 0) @@ -45,7 +49,6 @@ dec_counter() { echo "$val" > "$file" } -# Get counter value get_counter() { cat "$1" 2>/dev/null || echo 0 } @@ -56,62 +59,177 @@ update_iface_stats() { local proto="$2" local bytes="$3" + [ -z "$iface" ] && return + local stats=$(cat "$STATS_FILE_TMP" 2>/dev/null || echo "{}") - # Use jq to update stats (if available) or simple JSON if command -v jq >/dev/null 2>&1; then - stats=$(echo "$stats" | jq --arg iface "$iface" --arg proto "$proto" --argjson bytes "$bytes" ' + stats=$(echo "$stats" | jq --arg iface "$iface" --arg proto "$proto" --argjson bytes "${bytes:-0}" ' .[$iface] //= {"ip_bytes": 0, "wire_bytes": 0, "tcp": 0, "udp": 0, "icmp": 0} | .[$iface].ip_bytes += $bytes | .[$iface].wire_bytes += $bytes | - if $proto == "tcp" then .[$iface].tcp += 1 - elif $proto == "udp" then .[$iface].udp += 1 - elif $proto == "icmp" then .[$iface].icmp += 1 + if $proto == "tcp" or $proto == "6" then .[$iface].tcp += 1 + elif $proto == "udp" or $proto == "17" then .[$iface].udp += 1 + elif $proto == "icmp" or $proto == "1" then .[$iface].icmp += 1 else . end ') echo "$stats" > "$STATS_FILE_TMP" fi } +# Update application stats +update_app_stats() { + local app="$1" + local category="$2" + local bytes="$3" + + [ -z "$app" ] && return + + if command -v jq >/dev/null 2>&1; then + local apps=$(cat "$APPS_TMP" 2>/dev/null || echo "{}") + apps=$(echo "$apps" | jq --arg app "$app" --arg cat "${category:-Unknown}" --argjson bytes "${bytes:-0}" ' + .[$app] //= {"name": $app, "category": $cat, "flows": 0, "bytes": 0} | + .[$app].flows += 1 | + .[$app].bytes += $bytes + ') + echo "$apps" > "$APPS_TMP" + fi +} + +# Add flow to list +add_flow() { + local json="$1" + + if command -v jq >/dev/null 2>&1; then + local flow_info=$(echo "$json" | jq -c '{ + id: .flow_id, + src_ip: .src_ip, + src_port: .src_port, + dst_ip: .dst_ip, + dst_port: .dst_port, + proto: .l4_proto, + app: (.ndpi.app_proto // .ndpi.proto // "Unknown"), + category: (.ndpi.category // "Unknown"), + hostname: (.ndpi.hostname // .flow_dst_hostname // null), + confidence: (.ndpi.confidence // "Unknown"), + risk: (.ndpi.flow_risk // []), + bytes_rx: (.flow_src_tot_l4_payload_len // 0), + bytes_tx: (.flow_dst_tot_l4_payload_len // 0), + packets: ((.flow_src_packets_processed // 0) + (.flow_dst_packets_processed // 0)), + first_seen: .flow_first_seen, + last_seen: .flow_last_seen, + state: "active", + iface: .source + }' 2>/dev/null) + + [ -z "$flow_info" ] && return + + local flows=$(cat "$FLOWS_TMP" 2>/dev/null || echo "[]") + flows=$(echo "$flows" | jq --argjson flow "$flow_info" --argjson max "$MAX_FLOWS" ' + [. | .[] | select(.id != $flow.id)] + [$flow] | .[-$max:] + ') + echo "$flows" > "$FLOWS_TMP" + fi +} + +# Update existing flow +update_flow() { + local json="$1" + local flow_id=$(echo "$json" | jsonfilter -e '@.flow_id' 2>/dev/null) + + [ -z "$flow_id" ] && return + + if command -v jq >/dev/null 2>&1; then + local update_info=$(echo "$json" | jq -c '{ + bytes_rx: (.flow_src_tot_l4_payload_len // 0), + bytes_tx: (.flow_dst_tot_l4_payload_len // 0), + packets: ((.flow_src_packets_processed // 0) + (.flow_dst_packets_processed // 0)), + last_seen: .flow_last_seen + }' 2>/dev/null) + + local flows=$(cat "$FLOWS_TMP" 2>/dev/null || echo "[]") + flows=$(echo "$flows" | jq --arg id "$flow_id" --argjson update "$update_info" ' + map(if .id == ($id | tonumber) then . + $update else . end) + ') + echo "$flows" > "$FLOWS_TMP" + fi +} + +# Mark flow as ended +end_flow() { + local json="$1" + local flow_id=$(echo "$json" | jsonfilter -e '@.flow_id' 2>/dev/null) + + [ -z "$flow_id" ] && return + + if command -v jq >/dev/null 2>&1; then + local flows=$(cat "$FLOWS_TMP" 2>/dev/null || echo "[]") + flows=$(echo "$flows" | jq --arg id "$flow_id" ' + map(if .id == ($id | tonumber) then .state = "ended" else . end) + ') + echo "$flows" > "$FLOWS_TMP" + fi +} + # Process a single nDPId event process_event() { local raw="$1" - # Strip 5-digit length prefix - local json="${raw:5}" + # Strip 5-digit length prefix if present + local json="$raw" + if echo "$raw" | grep -q '^[0-9]\{5\}'; then + json="${raw:5}" + fi # Parse event type local event_name=$(echo "$json" | jsonfilter -e '@.flow_event_name' 2>/dev/null) [ -z "$event_name" ] && event_name=$(echo "$json" | jsonfilter -e '@.daemon_event_name' 2>/dev/null) + # Extract common fields + local iface=$(echo "$json" | jsonfilter -e '@.source' 2>/dev/null) + local proto=$(echo "$json" | jsonfilter -e '@.l4_proto' 2>/dev/null) + local src_bytes=$(echo "$json" | jsonfilter -e '@.flow_src_tot_l4_payload_len' 2>/dev/null || echo 0) + local dst_bytes=$(echo "$json" | jsonfilter -e '@.flow_dst_tot_l4_payload_len' 2>/dev/null || echo 0) + local total_bytes=$((src_bytes + dst_bytes)) + case "$event_name" in new) inc_counter "$FLOW_COUNT_FILE" inc_counter "$FLOWS_ACTIVE_FILE" + add_flow "$json" + ;; + detected|guessed|detection-update) + # Extract application info + local app=$(echo "$json" | jsonfilter -e '@.ndpi.app_proto' 2>/dev/null) + [ -z "$app" ] && app=$(echo "$json" | jsonfilter -e '@.ndpi.proto' 2>/dev/null) + local category=$(echo "$json" | jsonfilter -e '@.ndpi.category' 2>/dev/null) + + # Update stats + [ -n "$iface" ] && update_iface_stats "$iface" "$proto" "$total_bytes" + [ -n "$app" ] && update_app_stats "$app" "$category" "$total_bytes" + + # Update flow details + add_flow "$json" + ;; + update) + update_flow "$json" + [ -n "$iface" ] && update_iface_stats "$iface" "$proto" "$total_bytes" ;; end|idle) dec_counter "$FLOWS_ACTIVE_FILE" - ;; - detected|guessed) - # Extract flow info for stats - local iface=$(echo "$json" | jsonfilter -e '@.source' 2>/dev/null) - local proto=$(echo "$json" | jsonfilter -e '@.l4_proto' 2>/dev/null) - local src_bytes=$(echo "$json" | jsonfilter -e '@.flow_src_tot_l4_payload_len' 2>/dev/null || echo 0) - local dst_bytes=$(echo "$json" | jsonfilter -e '@.flow_dst_tot_l4_payload_len' 2>/dev/null || echo 0) - local total_bytes=$((src_bytes + dst_bytes)) - - [ -n "$iface" ] && update_iface_stats "$iface" "$proto" "$total_bytes" + end_flow "$json" ;; esac } -# Generate Netifyd-compatible status.json +# Generate status files generate_status() { local flow_count=$(get_counter "$FLOW_COUNT_FILE") local flows_active=$(get_counter "$FLOWS_ACTIVE_FILE") local stats=$(cat "$STATS_FILE_TMP" 2>/dev/null || echo "{}") local uptime=$(($(date +%s) - START_TIME)) + # Generate main status file if command -v jq >/dev/null 2>&1; then jq -n \ --argjson flow_count "$flow_count" \ @@ -127,6 +245,13 @@ generate_status() { uptime: $uptime, source: "ndpid-compat" }' > "$STATUS_FILE" + + # Generate flows file + cp "$FLOWS_TMP" "$FLOWS_FILE" 2>/dev/null + + # Generate apps file (sorted by bytes) + local apps=$(cat "$APPS_TMP" 2>/dev/null || echo "{}") + echo "$apps" | jq '[.[] | select(.name != null)] | sort_by(-.bytes) | .[0:100]' > "$APPS_FILE" 2>/dev/null else cat > "$STATUS_FILE" << EOF { @@ -142,21 +267,38 @@ EOF fi } +# Cleanup old ended flows +cleanup_flows() { + if command -v jq >/dev/null 2>&1; then + local flows=$(cat "$FLOWS_TMP" 2>/dev/null || echo "[]") + local now=$(date +%s) + # Keep active flows and ended flows from last 5 minutes + flows=$(echo "$flows" | jq --argjson now "$now" ' + [.[] | select(.state == "active" or (.last_seen != null and ($now - .last_seen) < 300))] + ') + echo "$flows" > "$FLOWS_TMP" + fi +} + # Main loop main() { START_TIME=$(date +%s) - logger -t ndpid-compat "Starting nDPId compatibility layer" + logger -t ndpid-compat "Starting nDPId compatibility layer (enhanced)" # Initialize state init_state - # Check for socat + # Check for dependencies if ! command -v socat >/dev/null 2>&1; then - logger -t ndpid-compat "ERROR: socat not found, using nc fallback" + logger -t ndpid-compat "WARNING: socat not found, using nc fallback" USE_NC=1 fi + if ! command -v jq >/dev/null 2>&1; then + logger -t ndpid-compat "WARNING: jq not found, detailed stats disabled" + fi + # Wait for distributor socket local wait_count=0 while [ ! -S "$DISTRIBUTOR_SOCK" ] && [ $wait_count -lt 30 ]; do @@ -179,7 +321,17 @@ main() { done ) & STATUS_PID=$! - trap "kill $STATUS_PID 2>/dev/null" EXIT + + # Background cleanup + ( + while true; do + sleep 60 + cleanup_flows + done + ) & + CLEANUP_PID=$! + + trap "kill $STATUS_PID $CLEANUP_PID 2>/dev/null" EXIT # Read events from distributor if [ -z "$USE_NC" ]; then @@ -193,11 +345,12 @@ main() { fi } -# Run main if not sourced +# Run modes case "$1" in -h|--help) echo "Usage: $0 [-d|--daemon]" - echo " Translates nDPId events to Netifyd-compatible format" + echo " Enhanced nDPId to Netifyd compatibility layer" + echo " Captures detailed flow and application information" exit 0 ;; -d|--daemon) diff --git a/package/secubox/luci-app-ndpid/root/usr/bin/ndpid-flow-actions b/package/secubox/luci-app-ndpid/root/usr/bin/ndpid-flow-actions old mode 100644 new mode 100755 diff --git a/package/secubox/luci-app-ndpid/root/usr/libexec/rpcd/luci.ndpid b/package/secubox/luci-app-ndpid/root/usr/libexec/rpcd/luci.ndpid index cddcfc48..38f02f79 100755 --- a/package/secubox/luci-app-ndpid/root/usr/libexec/rpcd/luci.ndpid +++ b/package/secubox/luci-app-ndpid/root/usr/libexec/rpcd/luci.ndpid @@ -13,6 +13,7 @@ STATUS_FILE="/var/run/netifyd/status.json" DISTRIBUTOR_SOCK="/var/run/ndpid/distributor.sock" COLLECTOR_SOCK="/var/run/ndpid/collector.sock" FLOWS_CACHE="/tmp/ndpid-flows.json" +APPS_CACHE="/tmp/ndpid-apps.json" STATS_CACHE="/tmp/ndpid-stats.json" LOG_FILE="/var/log/ndpid.log" @@ -138,31 +139,124 @@ get_interface_stats() { json_dump } -# Get top applications (from flow analysis) +# Get top applications (from aggregated apps file) get_top_applications() { json_init json_add_array "applications" - # Read from flows cache if available - if [ -f "$FLOWS_CACHE" ] && command -v jq >/dev/null 2>&1; then - # Aggregate by application - jq -r ' - group_by(.application) | + local app_data="" + + # Try apps cache first (pre-aggregated by ndpid-compat) + if [ -f "$APPS_CACHE" ] && command -v jq >/dev/null 2>&1; then + app_data=$(jq -r '.[0:15][] | "\(.name // "Unknown")|\(.category // "Unknown")|\(.flows // 0)|\(.bytes // 0)"' \ + "$APPS_CACHE" 2>/dev/null) + # Fallback to flows cache + elif [ -f "$FLOWS_CACHE" ] && command -v jq >/dev/null 2>&1; then + app_data=$(jq -r ' + group_by(.app) | map({ - name: .[0].application, + name: (.[0].app // "Unknown"), + category: (.[0].category // "Unknown"), flows: length, - bytes: (map(.bytes_rx + .bytes_tx) | add) + bytes: ([.[].bytes_rx, .[].bytes_tx] | add // 0) }) | sort_by(-.bytes) | - .[0:10][] | - "\(.name)|\(.flows)|\(.bytes)" - ' "$FLOWS_CACHE" 2>/dev/null | while IFS='|' read -r name flows bytes; do + .[0:15][] | + "\(.name)|\(.category)|\(.flows)|\(.bytes)" + ' "$FLOWS_CACHE" 2>/dev/null) + fi + + if [ -n "$app_data" ]; then + while IFS='|' read -r name category flows bytes; do + [ -z "$name" ] && continue json_add_object - json_add_string "name" "${name:-unknown}" + json_add_string "name" "${name:-Unknown}" + json_add_string "category" "${category:-Unknown}" json_add_int "flows" "${flows:-0}" json_add_int "bytes" "${bytes:-0}" json_close_object - done + done </dev/null 2>&1; then + # Get recent active flows sorted by bytes - use heredoc to avoid subshell + local flow_data + flow_data=$(jq -r ' + sort_by(-(.bytes_rx + .bytes_tx)) | + .[0:100][] | + "\(.id // 0)|\(.src_ip // "")|\(.src_port // 0)|\(.dst_ip // "")|\(.dst_port // 0)|\(.proto // "")|\(.app // "Unknown")|\(.category // "Unknown")|\(.hostname // "")|\(.confidence // "")|\(.bytes_rx // 0)|\(.bytes_tx // 0)|\(.packets // 0)|\(.state // "unknown")|\(.iface // "")" + ' "$FLOWS_CACHE" 2>/dev/null) + + while IFS='|' read -r id src_ip src_port dst_ip dst_port proto app category hostname confidence bytes_rx bytes_tx packets state iface; do + [ -z "$id" ] && continue + json_add_object + json_add_int "id" "${id:-0}" + json_add_string "src_ip" "${src_ip}" + json_add_int "src_port" "${src_port:-0}" + json_add_string "dst_ip" "${dst_ip}" + json_add_int "dst_port" "${dst_port:-0}" + json_add_string "proto" "${proto}" + json_add_string "app" "${app:-Unknown}" + json_add_string "category" "${category:-Unknown}" + json_add_string "hostname" "${hostname}" + json_add_string "confidence" "${confidence}" + json_add_int "bytes_rx" "${bytes_rx:-0}" + json_add_int "bytes_tx" "${bytes_tx:-0}" + json_add_int "packets" "${packets:-0}" + json_add_string "state" "${state:-unknown}" + json_add_string "iface" "${iface}" + json_close_object + done </dev/null 2>&1; then + local cat_data + cat_data=$(jq -r ' + group_by(.category) | + map({ + name: (.[0].category // "Unknown"), + apps: length, + flows: ([.[].flows] | add), + bytes: ([.[].bytes] | add) + }) | + sort_by(-.bytes) | + .[] | + "\(.name)|\(.apps)|\(.flows)|\(.bytes)" + ' "$APPS_CACHE" 2>/dev/null) + + while IFS='|' read -r name apps flows bytes; do + [ -z "$name" ] && continue + json_add_object + json_add_string "name" "${name:-Unknown}" + json_add_int "apps" "${apps:-0}" + json_add_int "flows" "${flows:-0}" + json_add_int "bytes" "${bytes:-0}" + json_close_object + done </dev/null | grep -E '^(br-|eth|wlan)'); do - json_add_string "" "$iface" + + # Get bridges and network interfaces from UCI + local net_ifaces="" + config_load network + + # Get all defined interfaces (br-lan, br-wan, etc) + for iface in $(ls /sys/class/net/ 2>/dev/null); do + case "$iface" in + lo|sit*|ip6*|gre*|ifb*|teql*) + # Skip loopback and virtual tunnel interfaces + ;; + br-*|eth*|wlan*|lan*|wan*) + # Include bridges, ethernet, wifi, and named interfaces + local state=$(cat /sys/class/net/$iface/operstate 2>/dev/null || echo "unknown") + local type=$(cat /sys/class/net/$iface/type 2>/dev/null || echo "0") + # Only include if it's a real interface (type 1 = ethernet) + if [ "$type" = "1" ] || [ -d "/sys/class/net/$iface/bridge" ]; then + json_add_object + json_add_string "name" "$iface" + json_add_string "state" "$state" + # Check if it's a bridge + if [ -d "/sys/class/net/$iface/bridge" ]; then + json_add_string "type" "bridge" + else + json_add_string "type" "interface" + fi + json_close_object + fi + ;; + esac done + json_close_array json_dump @@ -522,9 +645,11 @@ case "$1" in { "get_service_status": {}, "get_realtime_flows": {}, + "get_detailed_flows": {}, "get_interface_stats": {}, "get_top_applications": {}, "get_top_protocols": {}, + "get_categories": {}, "get_config": {}, "get_dashboard": {}, "get_interfaces": {}, @@ -546,6 +671,9 @@ EOF get_realtime_flows) get_realtime_flows ;; + get_detailed_flows) + get_detailed_flows + ;; get_interface_stats) get_interface_stats ;; @@ -555,6 +683,9 @@ EOF get_top_protocols) get_top_protocols ;; + get_categories) + get_categories + ;; get_config) get_config ;; diff --git a/package/secubox/luci-app-ndpid/root/usr/share/rpcd/acl.d/luci-app-ndpid.json b/package/secubox/luci-app-ndpid/root/usr/share/rpcd/acl.d/luci-app-ndpid.json index a81f2879..fea3f495 100644 --- a/package/secubox/luci-app-ndpid/root/usr/share/rpcd/acl.d/luci-app-ndpid.json +++ b/package/secubox/luci-app-ndpid/root/usr/share/rpcd/acl.d/luci-app-ndpid.json @@ -6,9 +6,11 @@ "luci.ndpid": [ "get_service_status", "get_realtime_flows", + "get_detailed_flows", "get_interface_stats", "get_top_applications", "get_top_protocols", + "get_categories", "get_config", "get_dashboard", "get_interfaces"