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: [],
|
cpuHistory: [],
|
||||||
memoryHistory: [],
|
memoryHistory: [],
|
||||||
diskHistory: [],
|
diskHistory: [],
|
||||||
|
loadHistory: [],
|
||||||
networkHistory: [],
|
networkHistory: [],
|
||||||
maxDataPoints: 60,
|
maxDataPoints: 60,
|
||||||
latestHealth: {},
|
latestHealth: {},
|
||||||
@ -50,15 +51,23 @@ return view.extend({
|
|||||||
addDataPoint: function(health) {
|
addDataPoint: function(health) {
|
||||||
var timestamp = Date.now();
|
var timestamp = Date.now();
|
||||||
|
|
||||||
this.cpuHistory.push({ time: timestamp, value: (health.cpu && health.cpu.percent) || 0 });
|
// API returns usage_percent, not percent
|
||||||
this.memoryHistory.push({ time: timestamp, value: (health.memory && health.memory.percent) || 0 });
|
this.cpuHistory.push({ time: timestamp, value: (health.cpu && (health.cpu.usage_percent || health.cpu.percent)) || 0 });
|
||||||
this.diskHistory.push({ time: timestamp, value: (health.disk && health.disk.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 netRx = (health.network && health.network.rx_bytes) || 0;
|
||||||
var netTx = (health.network && health.network.tx_bytes) || 0;
|
var netTx = (health.network && health.network.tx_bytes) || 0;
|
||||||
this.networkHistory.push({ time: timestamp, rx: netRx, tx: netTx });
|
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)
|
if (this[key].length > this.maxDataPoints)
|
||||||
this[key].shift();
|
this[key].shift();
|
||||||
}, this);
|
}, this);
|
||||||
@ -73,8 +82,8 @@ return view.extend({
|
|||||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox/monitoring.css') }),
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox/monitoring.css') }),
|
||||||
SecuNav.renderTabs('monitoring'),
|
SecuNav.renderTabs('monitoring'),
|
||||||
this.renderHeader(),
|
this.renderHeader(),
|
||||||
this.renderChartsGrid(),
|
this.renderCurrentStatsCard(),
|
||||||
this.renderCurrentStatsCard()
|
this.renderChartsGrid()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.updateCharts();
|
this.updateCharts();
|
||||||
@ -131,7 +140,7 @@ return view.extend({
|
|||||||
this.renderChartCard('cpu', _('CPU Usage'), '%', '#6366f1'),
|
this.renderChartCard('cpu', _('CPU Usage'), '%', '#6366f1'),
|
||||||
this.renderChartCard('memory', _('Memory Usage'), '%', '#22c55e'),
|
this.renderChartCard('memory', _('Memory Usage'), '%', '#22c55e'),
|
||||||
this.renderChartCard('disk', _('Disk Usage'), '%', '#f59e0b'),
|
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() {
|
renderStatsTable: function() {
|
||||||
var snapshot = this.getLatestSnapshot();
|
var snapshot = this.getLatestSnapshot();
|
||||||
var rates = this.getNetworkRateSummary();
|
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 = [
|
var stats = [
|
||||||
{ label: _('CPU Usage'), value: snapshot.cpu.value.toFixed(1) + '%', icon: '⚡' },
|
{ 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('cpu', this.cpuHistory, '#6366f1');
|
||||||
this.drawChart('memory', this.memoryHistory, '#22c55e');
|
this.drawChart('memory', this.memoryHistory, '#22c55e');
|
||||||
this.drawChart('disk', this.diskHistory, '#f59e0b');
|
this.drawChart('disk', this.diskHistory, '#f59e0b');
|
||||||
this.drawNetworkChart();
|
this.drawLoadChart();
|
||||||
},
|
},
|
||||||
|
|
||||||
drawChart: function(type, data, color) {
|
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() {
|
updateCurrentStats: function() {
|
||||||
var statsContainer = document.getElementById('current-stats');
|
var statsContainer = document.getElementById('current-stats');
|
||||||
if (statsContainer)
|
if (statsContainer)
|
||||||
|
|||||||
@ -7,8 +7,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=luci-app-ndpid
|
PKG_NAME:=luci-app-ndpid
|
||||||
PKG_VERSION:=0.9.1
|
PKG_VERSION:=1.0.0
|
||||||
PKG_RELEASE:=2
|
PKG_RELEASE:=1
|
||||||
PKG_ARCH:=all
|
PKG_ARCH:=all
|
||||||
|
|
||||||
PKG_LICENSE:=Apache-2.0
|
PKG_LICENSE:=Apache-2.0
|
||||||
|
|||||||
@ -20,6 +20,18 @@ var callRealtimeFlows = rpc.declare({
|
|||||||
expect: { }
|
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({
|
var callInterfaceStats = rpc.declare({
|
||||||
object: 'luci.ndpid',
|
object: 'luci.ndpid',
|
||||||
method: 'get_interface_stats',
|
method: 'get_interface_stats',
|
||||||
@ -136,9 +148,11 @@ return baseclass.extend({
|
|||||||
// Read methods
|
// Read methods
|
||||||
getServiceStatus: callServiceStatus,
|
getServiceStatus: callServiceStatus,
|
||||||
getRealtimeFlows: callRealtimeFlows,
|
getRealtimeFlows: callRealtimeFlows,
|
||||||
|
getDetailedFlows: callDetailedFlows,
|
||||||
getInterfaceStats: callInterfaceStats,
|
getInterfaceStats: callInterfaceStats,
|
||||||
getTopApplications: callTopApplications,
|
getTopApplications: callTopApplications,
|
||||||
getTopProtocols: callTopProtocols,
|
getTopProtocols: callTopProtocols,
|
||||||
|
getCategories: callCategories,
|
||||||
getConfig: callConfig,
|
getConfig: callConfig,
|
||||||
getDashboard: callDashboard,
|
getDashboard: callDashboard,
|
||||||
getInterfaces: callInterfaces,
|
getInterfaces: callInterfaces,
|
||||||
|
|||||||
@ -527,3 +527,276 @@
|
|||||||
background: var(--ndpi-border);
|
background: var(--ndpi-border);
|
||||||
border-radius: 4px;
|
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,
|
pollActive: true,
|
||||||
|
|
||||||
load: function() {
|
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) {
|
updateDashboard: function(data) {
|
||||||
@ -46,7 +95,7 @@ return view.extend({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Update interface stats
|
// Update interface stats
|
||||||
var interfaces = (data.interfaces || {}).interfaces || [];
|
var interfaces = Array.isArray(data.interfaces) ? data.interfaces : (data.interfaces || {}).interfaces || [];
|
||||||
interfaces.forEach(function(iface) {
|
interfaces.forEach(function(iface) {
|
||||||
var card = document.querySelector('.ndpi-iface-card[data-iface="' + iface.name + '"]');
|
var card = document.querySelector('.ndpi-iface-card[data-iface="' + iface.name + '"]');
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
@ -121,9 +170,11 @@ return view.extend({
|
|||||||
var service = dashboard.service || {};
|
var service = dashboard.service || {};
|
||||||
var flows = dashboard.flows || {};
|
var flows = dashboard.flows || {};
|
||||||
var system = dashboard.system || {};
|
var system = dashboard.system || {};
|
||||||
var interfaces = (data.interfaces || {}).interfaces || [];
|
// Handle both array and object formats from API
|
||||||
var applications = (data.applications || {}).applications || [];
|
var interfaces = Array.isArray(data.interfaces) ? data.interfaces : (data.interfaces || {}).interfaces || [];
|
||||||
var protocols = (data.protocols || {}).protocols || [];
|
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' }, [
|
var view = E('div', { 'class': 'ndpid-dashboard' }, [
|
||||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('ndpid/dashboard.css') }),
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('ndpid/dashboard.css') }),
|
||||||
@ -276,43 +327,81 @@ return view.extend({
|
|||||||
)
|
)
|
||||||
]),
|
]),
|
||||||
|
|
||||||
// Top Applications
|
// Grid layout for Applications and Categories
|
||||||
E('div', { 'class': 'ndpi-card' }, [
|
E('div', { 'class': 'ndpi-grid-2' }, [
|
||||||
E('div', { 'class': 'ndpi-card-header' }, [
|
// Top Applications
|
||||||
E('div', { 'class': 'ndpi-card-title' }, [
|
E('div', { 'class': 'ndpi-card' }, [
|
||||||
E('span', { 'class': 'ndpi-card-title-icon' }, '📱'),
|
E('div', { 'class': 'ndpi-card-header' }, [
|
||||||
'Top Applications'
|
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-card-badge' }, applications.length + ' detected')
|
||||||
E('div', { 'class': 'ndpi-table-container' }, [
|
]),
|
||||||
E('table', { 'class': 'ndpi-table' }, [
|
E('div', { 'class': 'ndpi-card-body' },
|
||||||
E('thead', {}, [
|
applications.length > 0 ?
|
||||||
E('tr', {}, [
|
E('div', { 'class': 'ndpi-apps-list' },
|
||||||
E('th', {}, 'Application'),
|
(function() {
|
||||||
E('th', {}, 'Flows'),
|
var maxBytes = Math.max.apply(null, applications.map(function(a) { return a.bytes || 0; })) || 1;
|
||||||
E('th', {}, 'Traffic')
|
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('tbody', {},
|
E('div', { 'class': 'ndpi-app-header' }, [
|
||||||
applications.map(function(app) {
|
E('span', { 'class': 'ndpi-app-icon' }, self.getAppIcon(app.name, app.category)),
|
||||||
return E('tr', {}, [
|
E('span', { 'class': 'ndpi-app-name' }, app.name || 'Unknown'),
|
||||||
E('td', {}, [
|
E('span', { 'class': 'ndpi-app-bytes' }, api.formatBytes(app.bytes || 0))
|
||||||
E('span', { 'class': 'ndpi-app-name' }, app.name || 'unknown')
|
|
||||||
]),
|
]),
|
||||||
E('td', { 'class': 'mono' }, api.formatNumber(app.flows)),
|
E('div', { 'class': 'ndpi-app-bar' }, [
|
||||||
E('td', { 'class': 'mono' }, api.formatBytes(app.bytes))
|
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
|
// Top Protocols
|
||||||
|
|||||||
@ -12,27 +12,85 @@ return view.extend({
|
|||||||
|
|
||||||
load: function() {
|
load: function() {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
api.getRealtimeFlows(),
|
api.getRealtimeFlows().catch(function(e) { console.log('getRealtimeFlows error:', e); return {}; }),
|
||||||
api.getInterfaceStats(),
|
api.getDetailedFlows().catch(function(e) { console.log('getDetailedFlows error:', e); return { flows: [] }; }),
|
||||||
api.getTopProtocols()
|
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) {
|
]).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 {
|
return {
|
||||||
flows: results[0],
|
status: results[0],
|
||||||
interfaces: results[1],
|
flows: results[1],
|
||||||
protocols: results[2]
|
applications: results[2],
|
||||||
|
categories: results[3],
|
||||||
|
interfaces: results[4],
|
||||||
|
protocols: results[5]
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
updateFlows: function(data) {
|
getProtoName: function(proto) {
|
||||||
var flows = data.flows || {};
|
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
|
// Update flow counts
|
||||||
|
var status = data.status || {};
|
||||||
var activeEl = document.querySelector('.ndpi-flows-active');
|
var activeEl = document.querySelector('.ndpi-flows-active');
|
||||||
var totalEl = document.querySelector('.ndpi-flows-total');
|
var totalEl = document.querySelector('.ndpi-flows-total');
|
||||||
|
|
||||||
if (activeEl) {
|
if (activeEl) {
|
||||||
var newActive = api.formatNumber(flows.flows_active || 0);
|
var newActive = api.formatNumber(status.flows_active || 0);
|
||||||
if (activeEl.textContent !== newActive) {
|
if (activeEl.textContent !== newActive) {
|
||||||
activeEl.textContent = newActive;
|
activeEl.textContent = newActive;
|
||||||
activeEl.classList.add('ndpi-value-updated');
|
activeEl.classList.add('ndpi-value-updated');
|
||||||
@ -41,23 +99,61 @@ return view.extend({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (totalEl) {
|
if (totalEl) {
|
||||||
var newTotal = api.formatNumber(flows.flow_count || 0);
|
totalEl.textContent = api.formatNumber(status.flow_count || 0);
|
||||||
if (totalEl.textContent !== newTotal) {
|
|
||||||
totalEl.textContent = newTotal;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update interface stats
|
// Update flows table
|
||||||
var interfaces = (data.interfaces || {}).interfaces || [];
|
var flows = Array.isArray(data.flows) ? data.flows : (data.flows || {}).flows || [];
|
||||||
interfaces.forEach(function(iface) {
|
var tbody = document.querySelector('.ndpi-flows-tbody');
|
||||||
var row = document.querySelector('.ndpi-iface-row[data-iface="' + iface.name + '"]');
|
if (tbody && flows.length > 0) {
|
||||||
if (!row) return;
|
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);
|
// Update top applications
|
||||||
row.querySelector('.ndpi-iface-udp').textContent = api.formatNumber(iface.udp);
|
var apps = Array.isArray(data.applications) ? data.applications : (data.applications || {}).applications || [];
|
||||||
row.querySelector('.ndpi-iface-icmp').textContent = api.formatNumber(iface.icmp);
|
var appsContainer = document.querySelector('.ndpi-apps-list');
|
||||||
row.querySelector('.ndpi-iface-bytes').textContent = api.formatBytes(iface.ip_bytes);
|
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() {
|
startPolling: function() {
|
||||||
@ -68,12 +164,14 @@ return view.extend({
|
|||||||
if (!this.pollActive) return Promise.resolve();
|
if (!this.pollActive) return Promise.resolve();
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
api.getRealtimeFlows(),
|
api.getRealtimeFlows().catch(function() { return {}; }),
|
||||||
api.getInterfaceStats()
|
api.getDetailedFlows().catch(function() { return { flows: [] }; }),
|
||||||
|
api.getTopApplications().catch(function() { return { applications: [] }; })
|
||||||
]).then(L.bind(function(results) {
|
]).then(L.bind(function(results) {
|
||||||
this.updateFlows({
|
this.updateData({
|
||||||
flows: results[0],
|
status: results[0],
|
||||||
interfaces: results[1]
|
flows: results[1],
|
||||||
|
applications: results[2]
|
||||||
});
|
});
|
||||||
}, this));
|
}, this));
|
||||||
}, this), this.pollInterval);
|
}, this), this.pollInterval);
|
||||||
@ -86,25 +184,37 @@ return view.extend({
|
|||||||
|
|
||||||
render: function(data) {
|
render: function(data) {
|
||||||
var self = this;
|
var self = this;
|
||||||
var flows = data.flows || {};
|
var status = data.status || {};
|
||||||
var interfaces = (data.interfaces || {}).interfaces || [];
|
|
||||||
var protocols = (data.protocols || {}).protocols || [];
|
// 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 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') }),
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('ndpid/dashboard.css') }),
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
E('div', { 'class': 'ndpi-header' }, [
|
E('div', { 'class': 'ndpi-header' }, [
|
||||||
E('div', { 'class': 'ndpi-logo' }, [
|
E('div', { 'class': 'ndpi-logo' }, [
|
||||||
E('div', { 'class': 'ndpi-logo-icon' }, '📊'),
|
E('div', { 'class': 'ndpi-logo-icon' }, '🔍'),
|
||||||
E('div', { 'class': 'ndpi-logo-text' }, ['Flow ', E('span', {}, 'Statistics')])
|
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-stats' }, [
|
||||||
E('div', { 'class': 'ndpi-quick-stat', 'style': '--stat-gradient: linear-gradient(135deg, #10b981, #34d399)' }, [
|
E('div', { 'class': 'ndpi-quick-stat', 'style': '--stat-gradient: linear-gradient(135deg, #10b981, #34d399)' }, [
|
||||||
E('div', { 'class': 'ndpi-quick-stat-header' }, [
|
E('div', { 'class': 'ndpi-quick-stat-header' }, [
|
||||||
@ -112,8 +222,8 @@ return view.extend({
|
|||||||
E('span', { 'class': 'ndpi-quick-stat-label' }, 'Active Flows')
|
E('span', { 'class': 'ndpi-quick-stat-label' }, 'Active Flows')
|
||||||
]),
|
]),
|
||||||
E('div', { 'class': 'ndpi-quick-stat-value ndpi-flows-active' },
|
E('div', { 'class': 'ndpi-quick-stat-value ndpi-flows-active' },
|
||||||
api.formatNumber(flows.flows_active || 0)),
|
api.formatNumber(status.flows_active || 0)),
|
||||||
E('div', { 'class': 'ndpi-quick-stat-sub' }, 'Currently tracked')
|
E('div', { 'class': 'ndpi-quick-stat-sub' }, 'Real-time tracking')
|
||||||
]),
|
]),
|
||||||
E('div', { 'class': 'ndpi-quick-stat' }, [
|
E('div', { 'class': 'ndpi-quick-stat' }, [
|
||||||
E('div', { 'class': 'ndpi-quick-stat-header' }, [
|
E('div', { 'class': 'ndpi-quick-stat-header' }, [
|
||||||
@ -121,109 +231,197 @@ return view.extend({
|
|||||||
E('span', { 'class': 'ndpi-quick-stat-label' }, 'Total Flows')
|
E('span', { 'class': 'ndpi-quick-stat-label' }, 'Total Flows')
|
||||||
]),
|
]),
|
||||||
E('div', { 'class': 'ndpi-quick-stat-value ndpi-flows-total' },
|
E('div', { 'class': 'ndpi-quick-stat-value ndpi-flows-total' },
|
||||||
api.formatNumber(flows.flow_count || 0)),
|
api.formatNumber(status.flow_count || 0)),
|
||||||
E('div', { 'class': 'ndpi-quick-stat-sub' }, 'Since service start')
|
E('div', { 'class': 'ndpi-quick-stat-sub' }, 'Since start')
|
||||||
]),
|
]),
|
||||||
E('div', { 'class': 'ndpi-quick-stat' }, [
|
E('div', { 'class': 'ndpi-quick-stat' }, [
|
||||||
E('div', { 'class': 'ndpi-quick-stat-header' }, [
|
E('div', { 'class': 'ndpi-quick-stat-header' }, [
|
||||||
E('span', { 'class': 'ndpi-quick-stat-icon' }, '📦'),
|
E('span', { 'class': 'ndpi-quick-stat-icon' }, '📱'),
|
||||||
E('span', { 'class': 'ndpi-quick-stat-label' }, 'Total Packets')
|
E('span', { 'class': 'ndpi-quick-stat-label' }, 'Applications')
|
||||||
]),
|
]),
|
||||||
E('div', { 'class': 'ndpi-quick-stat-value' },
|
E('div', { 'class': 'ndpi-quick-stat-value' },
|
||||||
api.formatNumber(totalPackets)),
|
api.formatNumber(applications.length)),
|
||||||
E('div', { 'class': 'ndpi-quick-stat-sub' }, 'TCP + UDP + ICMP')
|
E('div', { 'class': 'ndpi-quick-stat-sub' }, 'Detected')
|
||||||
]),
|
]),
|
||||||
E('div', { 'class': 'ndpi-quick-stat' }, [
|
E('div', { 'class': 'ndpi-quick-stat' }, [
|
||||||
E('div', { 'class': 'ndpi-quick-stat-header' }, [
|
E('div', { 'class': 'ndpi-quick-stat-header' }, [
|
||||||
E('span', { 'class': 'ndpi-quick-stat-icon' }, '⏱'),
|
E('span', { 'class': 'ndpi-quick-stat-icon' }, '🏷️'),
|
||||||
E('span', { 'class': 'ndpi-quick-stat-label' }, 'Uptime')
|
E('span', { 'class': 'ndpi-quick-stat-label' }, 'Categories')
|
||||||
]),
|
]),
|
||||||
E('div', { 'class': 'ndpi-quick-stat-value' },
|
E('div', { 'class': 'ndpi-quick-stat-value' },
|
||||||
api.formatUptime(flows.uptime || 0)),
|
api.formatNumber(categories.length)),
|
||||||
E('div', { 'class': 'ndpi-quick-stat-sub' }, 'Service runtime')
|
E('div', { 'class': 'ndpi-quick-stat-sub' }, 'Traffic types')
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
|
|
||||||
// Interface Statistics Table
|
// Main content grid
|
||||||
E('div', { 'class': 'ndpi-card' }, [
|
E('div', { 'class': 'ndpi-grid-2' }, [
|
||||||
E('div', { 'class': 'ndpi-card-header' }, [
|
// Flows Table
|
||||||
E('div', { 'class': 'ndpi-card-title' }, [
|
E('div', { 'class': 'ndpi-card ndpi-card-wide' }, [
|
||||||
E('span', { 'class': 'ndpi-card-title-icon' }, '🌐'),
|
E('div', { 'class': 'ndpi-card-header' }, [
|
||||||
'Per-Interface Statistics'
|
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' },
|
E('div', { 'class': 'ndpi-card-body ndpi-flows-table-container' },
|
||||||
interfaces.length + ' interface' + (interfaces.length !== 1 ? 's' : ''))
|
flows.length > 0 ?
|
||||||
]),
|
E('table', { 'class': 'ndpi-table ndpi-flows-table' }, [
|
||||||
E('div', { 'class': 'ndpi-card-body' },
|
|
||||||
interfaces.length > 0 ?
|
|
||||||
E('div', { 'class': 'ndpi-table-container' }, [
|
|
||||||
E('table', { 'class': 'ndpi-table' }, [
|
|
||||||
E('thead', {}, [
|
E('thead', {}, [
|
||||||
E('tr', {}, [
|
E('tr', {}, [
|
||||||
E('th', {}, 'Interface'),
|
E('th', {}, 'Application'),
|
||||||
E('th', {}, 'TCP'),
|
E('th', {}, 'Source'),
|
||||||
E('th', {}, 'UDP'),
|
E('th', {}, ''),
|
||||||
E('th', {}, 'ICMP'),
|
E('th', {}, 'Destination'),
|
||||||
E('th', {}, 'Total Bytes')
|
E('th', {}, 'Proto'),
|
||||||
|
E('th', {}, 'Category'),
|
||||||
|
E('th', {}, 'Traffic'),
|
||||||
|
E('th', {}, '')
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
E('tbody', {},
|
E('tbody', { 'class': 'ndpi-flows-tbody' },
|
||||||
interfaces.map(function(iface) {
|
flows.slice(0, 50).map(function(flow) {
|
||||||
return E('tr', { 'class': 'ndpi-iface-row', 'data-iface': iface.name }, [
|
return E('tr', { 'class': 'ndpi-flow-row ' + (flow.state === 'ended' ? 'ndpi-flow-ended' : 'ndpi-flow-active') }, [
|
||||||
E('td', {}, [
|
E('td', { 'class': 'ndpi-flow-app' }, [
|
||||||
E('span', { 'class': 'ndpi-app-name' }, iface.name)
|
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': 'ndpi-flow-src mono' }, flow.src_ip + ':' + flow.src_port),
|
||||||
E('td', { 'class': 'mono ndpi-iface-udp' }, api.formatNumber(iface.udp)),
|
E('td', { 'class': 'ndpi-flow-arrow' }, '→'),
|
||||||
E('td', { 'class': 'mono ndpi-iface-icmp' }, api.formatNumber(iface.icmp)),
|
E('td', { 'class': 'ndpi-flow-dst mono' }, flow.dst_ip + ':' + flow.dst_port),
|
||||||
E('td', { 'class': 'mono ndpi-iface-bytes' }, api.formatBytes(iface.ip_bytes))
|
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
|
// Protocol & Category breakdown
|
||||||
E('div', { 'class': 'ndpi-card' }, [
|
E('div', { 'class': 'ndpi-grid-2' }, [
|
||||||
E('div', { 'class': 'ndpi-card-header' }, [
|
// Protocol Distribution
|
||||||
E('div', { 'class': 'ndpi-card-title' }, [
|
E('div', { 'class': 'ndpi-card' }, [
|
||||||
E('span', { 'class': 'ndpi-card-title-icon' }, '📡'),
|
E('div', { 'class': 'ndpi-card-header' }, [
|
||||||
'Protocol Breakdown'
|
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 ?
|
// Categories
|
||||||
E('div', { 'class': 'ndpi-protocol-grid' },
|
E('div', { 'class': 'ndpi-card' }, [
|
||||||
protocols.map(function(proto) {
|
E('div', { 'class': 'ndpi-card-header' }, [
|
||||||
var pct = totalPackets > 0 ? Math.round((proto.count / totalPackets) * 100) : 0;
|
E('div', { 'class': 'ndpi-card-title' }, [
|
||||||
var color = proto.name === 'TCP' ? '#0ea5e9' :
|
E('span', { 'class': 'ndpi-card-title-icon' }, '🏷️'),
|
||||||
proto.name === 'UDP' ? '#10b981' : '#f59e0b';
|
'Traffic Categories'
|
||||||
return E('div', { 'class': 'ndpi-protocol-item' }, [
|
])
|
||||||
E('div', { 'class': 'ndpi-protocol-header' }, [
|
]),
|
||||||
E('span', { 'class': 'ndpi-protocol-name' }, proto.name),
|
E('div', { 'class': 'ndpi-card-body' },
|
||||||
E('span', { 'class': 'ndpi-protocol-count' }, api.formatNumber(proto.count))
|
categories.length > 0 ?
|
||||||
]),
|
E('div', { 'class': 'ndpi-categories-list' },
|
||||||
E('div', { 'class': 'ndpi-protocol-bar' }, [
|
(function() {
|
||||||
E('div', {
|
var maxBytes = Math.max.apply(null, categories.map(function(c) { return c.bytes || 0; })) || 1;
|
||||||
'class': 'ndpi-protocol-bar-fill',
|
return categories.slice(0, 8).map(function(cat) {
|
||||||
'style': 'width: ' + pct + '%; background: ' + color
|
var pct = Math.round(((cat.bytes || 0) / maxBytes) * 100);
|
||||||
})
|
return E('div', { 'class': 'ndpi-category-item' }, [
|
||||||
]),
|
E('div', { 'class': 'ndpi-category-header' }, [
|
||||||
E('div', { 'class': 'ndpi-protocol-pct' }, pct + '%')
|
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-empty' }, [
|
E('div', { 'class': 'ndpi-category-bar-fill', 'style': 'width:' + pct + '%;background:' + self.getCategoryColor(cat.name) })
|
||||||
E('div', { 'class': 'ndpi-empty-icon' }, '📡'),
|
]),
|
||||||
E('div', { 'class': 'ndpi-empty-text' }, 'No protocol data available')
|
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
|
#!/bin/sh
|
||||||
# nDPId to Netifyd Compatibility Layer
|
# 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
|
# Copyright (C) 2025 CyberMind.fr
|
||||||
|
|
||||||
. /lib/functions.sh
|
. /lib/functions.sh
|
||||||
@ -10,16 +10,19 @@
|
|||||||
DISTRIBUTOR_SOCK="/var/run/ndpid/distributor.sock"
|
DISTRIBUTOR_SOCK="/var/run/ndpid/distributor.sock"
|
||||||
STATUS_FILE="/var/run/netifyd/status.json"
|
STATUS_FILE="/var/run/netifyd/status.json"
|
||||||
FLOWS_FILE="/tmp/ndpid-flows.json"
|
FLOWS_FILE="/tmp/ndpid-flows.json"
|
||||||
|
APPS_FILE="/tmp/ndpid-apps.json"
|
||||||
STATS_FILE="/tmp/ndpid-stats.json"
|
STATS_FILE="/tmp/ndpid-stats.json"
|
||||||
STATS_HISTORY="/tmp/ndpid-stats-history.json"
|
|
||||||
UPDATE_INTERVAL=1
|
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"
|
STATE_DIR="/tmp/ndpid-state"
|
||||||
FLOWS_ACTIVE_FILE="$STATE_DIR/flows_active"
|
FLOWS_ACTIVE_FILE="$STATE_DIR/flows_active"
|
||||||
FLOW_COUNT_FILE="$STATE_DIR/flow_count"
|
FLOW_COUNT_FILE="$STATE_DIR/flow_count"
|
||||||
STATS_FILE_TMP="$STATE_DIR/stats"
|
STATS_FILE_TMP="$STATE_DIR/stats"
|
||||||
|
FLOWS_TMP="$STATE_DIR/flows"
|
||||||
|
APPS_TMP="$STATE_DIR/apps"
|
||||||
|
|
||||||
# Initialize state
|
# Initialize state
|
||||||
init_state() {
|
init_state() {
|
||||||
@ -28,16 +31,17 @@ init_state() {
|
|||||||
echo "0" > "$FLOWS_ACTIVE_FILE"
|
echo "0" > "$FLOWS_ACTIVE_FILE"
|
||||||
echo "0" > "$FLOW_COUNT_FILE"
|
echo "0" > "$FLOW_COUNT_FILE"
|
||||||
echo "{}" > "$STATS_FILE_TMP"
|
echo "{}" > "$STATS_FILE_TMP"
|
||||||
|
echo "[]" > "$FLOWS_TMP"
|
||||||
|
echo "{}" > "$APPS_TMP"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Increment counter
|
# Counter operations
|
||||||
inc_counter() {
|
inc_counter() {
|
||||||
local file="$1"
|
local file="$1"
|
||||||
local val=$(cat "$file" 2>/dev/null || echo 0)
|
local val=$(cat "$file" 2>/dev/null || echo 0)
|
||||||
echo $((val + 1)) > "$file"
|
echo $((val + 1)) > "$file"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Decrement counter
|
|
||||||
dec_counter() {
|
dec_counter() {
|
||||||
local file="$1"
|
local file="$1"
|
||||||
local val=$(cat "$file" 2>/dev/null || echo 0)
|
local val=$(cat "$file" 2>/dev/null || echo 0)
|
||||||
@ -45,7 +49,6 @@ dec_counter() {
|
|||||||
echo "$val" > "$file"
|
echo "$val" > "$file"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get counter value
|
|
||||||
get_counter() {
|
get_counter() {
|
||||||
cat "$1" 2>/dev/null || echo 0
|
cat "$1" 2>/dev/null || echo 0
|
||||||
}
|
}
|
||||||
@ -56,62 +59,177 @@ update_iface_stats() {
|
|||||||
local proto="$2"
|
local proto="$2"
|
||||||
local bytes="$3"
|
local bytes="$3"
|
||||||
|
|
||||||
|
[ -z "$iface" ] && return
|
||||||
|
|
||||||
local stats=$(cat "$STATS_FILE_TMP" 2>/dev/null || echo "{}")
|
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
|
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": 0, "wire_bytes": 0, "tcp": 0, "udp": 0, "icmp": 0} |
|
||||||
.[$iface].ip_bytes += $bytes |
|
.[$iface].ip_bytes += $bytes |
|
||||||
.[$iface].wire_bytes += $bytes |
|
.[$iface].wire_bytes += $bytes |
|
||||||
if $proto == "tcp" then .[$iface].tcp += 1
|
if $proto == "tcp" or $proto == "6" then .[$iface].tcp += 1
|
||||||
elif $proto == "udp" then .[$iface].udp += 1
|
elif $proto == "udp" or $proto == "17" then .[$iface].udp += 1
|
||||||
elif $proto == "icmp" then .[$iface].icmp += 1
|
elif $proto == "icmp" or $proto == "1" then .[$iface].icmp += 1
|
||||||
else . end
|
else . end
|
||||||
')
|
')
|
||||||
echo "$stats" > "$STATS_FILE_TMP"
|
echo "$stats" > "$STATS_FILE_TMP"
|
||||||
fi
|
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 a single nDPId event
|
||||||
process_event() {
|
process_event() {
|
||||||
local raw="$1"
|
local raw="$1"
|
||||||
|
|
||||||
# Strip 5-digit length prefix
|
# Strip 5-digit length prefix if present
|
||||||
local json="${raw:5}"
|
local json="$raw"
|
||||||
|
if echo "$raw" | grep -q '^[0-9]\{5\}'; then
|
||||||
|
json="${raw:5}"
|
||||||
|
fi
|
||||||
|
|
||||||
# Parse event type
|
# Parse event type
|
||||||
local event_name=$(echo "$json" | jsonfilter -e '@.flow_event_name' 2>/dev/null)
|
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)
|
[ -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
|
case "$event_name" in
|
||||||
new)
|
new)
|
||||||
inc_counter "$FLOW_COUNT_FILE"
|
inc_counter "$FLOW_COUNT_FILE"
|
||||||
inc_counter "$FLOWS_ACTIVE_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)
|
end|idle)
|
||||||
dec_counter "$FLOWS_ACTIVE_FILE"
|
dec_counter "$FLOWS_ACTIVE_FILE"
|
||||||
;;
|
end_flow "$json"
|
||||||
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"
|
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
# Generate Netifyd-compatible status.json
|
# Generate status files
|
||||||
generate_status() {
|
generate_status() {
|
||||||
local flow_count=$(get_counter "$FLOW_COUNT_FILE")
|
local flow_count=$(get_counter "$FLOW_COUNT_FILE")
|
||||||
local flows_active=$(get_counter "$FLOWS_ACTIVE_FILE")
|
local flows_active=$(get_counter "$FLOWS_ACTIVE_FILE")
|
||||||
local stats=$(cat "$STATS_FILE_TMP" 2>/dev/null || echo "{}")
|
local stats=$(cat "$STATS_FILE_TMP" 2>/dev/null || echo "{}")
|
||||||
local uptime=$(($(date +%s) - START_TIME))
|
local uptime=$(($(date +%s) - START_TIME))
|
||||||
|
|
||||||
|
# Generate main status file
|
||||||
if command -v jq >/dev/null 2>&1; then
|
if command -v jq >/dev/null 2>&1; then
|
||||||
jq -n \
|
jq -n \
|
||||||
--argjson flow_count "$flow_count" \
|
--argjson flow_count "$flow_count" \
|
||||||
@ -127,6 +245,13 @@ generate_status() {
|
|||||||
uptime: $uptime,
|
uptime: $uptime,
|
||||||
source: "ndpid-compat"
|
source: "ndpid-compat"
|
||||||
}' > "$STATUS_FILE"
|
}' > "$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
|
else
|
||||||
cat > "$STATUS_FILE" << EOF
|
cat > "$STATUS_FILE" << EOF
|
||||||
{
|
{
|
||||||
@ -142,21 +267,38 @@ EOF
|
|||||||
fi
|
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 loop
|
||||||
main() {
|
main() {
|
||||||
START_TIME=$(date +%s)
|
START_TIME=$(date +%s)
|
||||||
|
|
||||||
logger -t ndpid-compat "Starting nDPId compatibility layer"
|
logger -t ndpid-compat "Starting nDPId compatibility layer (enhanced)"
|
||||||
|
|
||||||
# Initialize state
|
# Initialize state
|
||||||
init_state
|
init_state
|
||||||
|
|
||||||
# Check for socat
|
# Check for dependencies
|
||||||
if ! command -v socat >/dev/null 2>&1; then
|
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
|
USE_NC=1
|
||||||
fi
|
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
|
# Wait for distributor socket
|
||||||
local wait_count=0
|
local wait_count=0
|
||||||
while [ ! -S "$DISTRIBUTOR_SOCK" ] && [ $wait_count -lt 30 ]; do
|
while [ ! -S "$DISTRIBUTOR_SOCK" ] && [ $wait_count -lt 30 ]; do
|
||||||
@ -179,7 +321,17 @@ main() {
|
|||||||
done
|
done
|
||||||
) &
|
) &
|
||||||
STATUS_PID=$!
|
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
|
# Read events from distributor
|
||||||
if [ -z "$USE_NC" ]; then
|
if [ -z "$USE_NC" ]; then
|
||||||
@ -193,11 +345,12 @@ main() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Run main if not sourced
|
# Run modes
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-h|--help)
|
-h|--help)
|
||||||
echo "Usage: $0 [-d|--daemon]"
|
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
|
exit 0
|
||||||
;;
|
;;
|
||||||
-d|--daemon)
|
-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"
|
DISTRIBUTOR_SOCK="/var/run/ndpid/distributor.sock"
|
||||||
COLLECTOR_SOCK="/var/run/ndpid/collector.sock"
|
COLLECTOR_SOCK="/var/run/ndpid/collector.sock"
|
||||||
FLOWS_CACHE="/tmp/ndpid-flows.json"
|
FLOWS_CACHE="/tmp/ndpid-flows.json"
|
||||||
|
APPS_CACHE="/tmp/ndpid-apps.json"
|
||||||
STATS_CACHE="/tmp/ndpid-stats.json"
|
STATS_CACHE="/tmp/ndpid-stats.json"
|
||||||
LOG_FILE="/var/log/ndpid.log"
|
LOG_FILE="/var/log/ndpid.log"
|
||||||
|
|
||||||
@ -138,31 +139,124 @@ get_interface_stats() {
|
|||||||
json_dump
|
json_dump
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get top applications (from flow analysis)
|
# Get top applications (from aggregated apps file)
|
||||||
get_top_applications() {
|
get_top_applications() {
|
||||||
json_init
|
json_init
|
||||||
json_add_array "applications"
|
json_add_array "applications"
|
||||||
|
|
||||||
# Read from flows cache if available
|
local app_data=""
|
||||||
if [ -f "$FLOWS_CACHE" ] && command -v jq >/dev/null 2>&1; then
|
|
||||||
# Aggregate by application
|
# Try apps cache first (pre-aggregated by ndpid-compat)
|
||||||
jq -r '
|
if [ -f "$APPS_CACHE" ] && command -v jq >/dev/null 2>&1; then
|
||||||
group_by(.application) |
|
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({
|
map({
|
||||||
name: .[0].application,
|
name: (.[0].app // "Unknown"),
|
||||||
|
category: (.[0].category // "Unknown"),
|
||||||
flows: length,
|
flows: length,
|
||||||
bytes: (map(.bytes_rx + .bytes_tx) | add)
|
bytes: ([.[].bytes_rx, .[].bytes_tx] | add // 0)
|
||||||
}) |
|
}) |
|
||||||
sort_by(-.bytes) |
|
sort_by(-.bytes) |
|
||||||
.[0:10][] |
|
.[0:15][] |
|
||||||
"\(.name)|\(.flows)|\(.bytes)"
|
"\(.name)|\(.category)|\(.flows)|\(.bytes)"
|
||||||
' "$FLOWS_CACHE" 2>/dev/null | while IFS='|' read -r name flows bytes; do
|
' "$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_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 "flows" "${flows:-0}"
|
||||||
json_add_int "bytes" "${bytes:-0}"
|
json_add_int "bytes" "${bytes:-0}"
|
||||||
json_close_object
|
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
|
fi
|
||||||
|
|
||||||
json_close_array
|
json_close_array
|
||||||
@ -505,11 +599,40 @@ get_interfaces() {
|
|||||||
|
|
||||||
json_close_array
|
json_close_array
|
||||||
|
|
||||||
# Also list available interfaces
|
# Scan available interfaces from network config and system
|
||||||
json_add_array "available"
|
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
|
done
|
||||||
|
|
||||||
json_close_array
|
json_close_array
|
||||||
|
|
||||||
json_dump
|
json_dump
|
||||||
@ -522,9 +645,11 @@ case "$1" in
|
|||||||
{
|
{
|
||||||
"get_service_status": {},
|
"get_service_status": {},
|
||||||
"get_realtime_flows": {},
|
"get_realtime_flows": {},
|
||||||
|
"get_detailed_flows": {},
|
||||||
"get_interface_stats": {},
|
"get_interface_stats": {},
|
||||||
"get_top_applications": {},
|
"get_top_applications": {},
|
||||||
"get_top_protocols": {},
|
"get_top_protocols": {},
|
||||||
|
"get_categories": {},
|
||||||
"get_config": {},
|
"get_config": {},
|
||||||
"get_dashboard": {},
|
"get_dashboard": {},
|
||||||
"get_interfaces": {},
|
"get_interfaces": {},
|
||||||
@ -546,6 +671,9 @@ EOF
|
|||||||
get_realtime_flows)
|
get_realtime_flows)
|
||||||
get_realtime_flows
|
get_realtime_flows
|
||||||
;;
|
;;
|
||||||
|
get_detailed_flows)
|
||||||
|
get_detailed_flows
|
||||||
|
;;
|
||||||
get_interface_stats)
|
get_interface_stats)
|
||||||
get_interface_stats
|
get_interface_stats
|
||||||
;;
|
;;
|
||||||
@ -555,6 +683,9 @@ EOF
|
|||||||
get_top_protocols)
|
get_top_protocols)
|
||||||
get_top_protocols
|
get_top_protocols
|
||||||
;;
|
;;
|
||||||
|
get_categories)
|
||||||
|
get_categories
|
||||||
|
;;
|
||||||
get_config)
|
get_config)
|
||||||
get_config
|
get_config
|
||||||
;;
|
;;
|
||||||
|
|||||||
@ -6,9 +6,11 @@
|
|||||||
"luci.ndpid": [
|
"luci.ndpid": [
|
||||||
"get_service_status",
|
"get_service_status",
|
||||||
"get_realtime_flows",
|
"get_realtime_flows",
|
||||||
|
"get_detailed_flows",
|
||||||
"get_interface_stats",
|
"get_interface_stats",
|
||||||
"get_top_applications",
|
"get_top_applications",
|
||||||
"get_top_protocols",
|
"get_top_protocols",
|
||||||
|
"get_categories",
|
||||||
"get_config",
|
"get_config",
|
||||||
"get_dashboard",
|
"get_dashboard",
|
||||||
"get_interfaces"
|
"get_interfaces"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user