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 <noreply@anthropic.com>
This commit is contained in:
parent
66f4f32655
commit
50bd0c872e
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 = [
|
||||
'<td class="ndpi-flow-app">',
|
||||
'<span class="ndpi-app-icon">' + self.getAppIcon(flow.app, flow.category) + '</span>',
|
||||
'<div class="ndpi-app-info">',
|
||||
'<span class="ndpi-app-name">' + (flow.app || 'Unknown') + '</span>',
|
||||
'<span class="ndpi-app-host">' + (flow.hostname || '') + '</span>',
|
||||
'</div>',
|
||||
'</td>',
|
||||
'<td class="ndpi-flow-src mono">' + flow.src_ip + ':' + flow.src_port + '</td>',
|
||||
'<td class="ndpi-flow-arrow">→</td>',
|
||||
'<td class="ndpi-flow-dst mono">' + flow.dst_ip + ':' + flow.dst_port + '</td>',
|
||||
'<td class="ndpi-flow-proto"><span class="ndpi-proto-badge ndpi-proto-' + self.getProtoName(flow.proto).toLowerCase() + '">' + self.getProtoName(flow.proto) + '</span></td>',
|
||||
'<td class="ndpi-flow-category"><span class="ndpi-category-badge" style="background:' + self.getCategoryColor(flow.category) + '">' + (flow.category || 'Unknown') + '</span></td>',
|
||||
'<td class="ndpi-flow-bytes mono">' + api.formatBytes((flow.bytes_rx || 0) + (flow.bytes_tx || 0)) + '</td>',
|
||||
'<td class="ndpi-flow-state"><span class="ndpi-state-' + flow.state + '">' + (flow.state === 'active' ? '●' : '○') + '</span></td>'
|
||||
].join('');
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
} else if (tbody) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="ndpi-empty-row">No flows detected yet</td></tr>';
|
||||
}
|
||||
|
||||
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 = [
|
||||
'<div class="ndpi-app-header">',
|
||||
'<span class="ndpi-app-icon">' + self.getAppIcon(app.name, app.category) + '</span>',
|
||||
'<span class="ndpi-app-name">' + app.name + '</span>',
|
||||
'<span class="ndpi-app-bytes">' + api.formatBytes(app.bytes || 0) + '</span>',
|
||||
'</div>',
|
||||
'<div class="ndpi-app-bar"><div class="ndpi-app-bar-fill" style="width:' + pct + '%;background:' + self.getCategoryColor(app.category) + '"></div></div>',
|
||||
'<div class="ndpi-app-meta">' + (app.flows || 0) + ' flows · ' + (app.category || 'Unknown') + '</div>'
|
||||
].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')
|
||||
])
|
||||
)
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
|
||||
0
package/secubox/luci-app-ndpid/root/usr/bin/ndpid-collector
Normal file → Executable file
0
package/secubox/luci-app-ndpid/root/usr/bin/ndpid-collector
Normal file → Executable file
@ -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)
|
||||
|
||||
0
package/secubox/luci-app-ndpid/root/usr/bin/ndpid-flow-actions
Normal file → Executable file
0
package/secubox/luci-app-ndpid/root/usr/bin/ndpid-flow-actions
Normal file → Executable file
@ -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 <<EOF
|
||||
$app_data
|
||||
EOF
|
||||
fi
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Get detailed flows with detection info
|
||||
get_detailed_flows() {
|
||||
json_init
|
||||
json_add_array "flows"
|
||||
|
||||
if [ -f "$FLOWS_CACHE" ] && command -v jq >/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 <<EOF
|
||||
$flow_data
|
||||
EOF
|
||||
fi
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Get category breakdown
|
||||
get_categories() {
|
||||
json_init
|
||||
json_add_array "categories"
|
||||
|
||||
if [ -f "$APPS_CACHE" ] && command -v jq >/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 <<EOF
|
||||
$cat_data
|
||||
EOF
|
||||
fi
|
||||
|
||||
json_close_array
|
||||
@ -505,11 +599,40 @@ get_interfaces() {
|
||||
|
||||
json_close_array
|
||||
|
||||
# Also list available interfaces
|
||||
# Scan available interfaces from network config and system
|
||||
json_add_array "available"
|
||||
for iface in $(ls /sys/class/net/ 2>/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
|
||||
;;
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user