feat: Add nDPId package for lightweight DPI (alternative to netifyd)

- Add secubox-app-ndpid: nDPId daemon with bundled libndpi 5.x
- Add luci-app-ndpid: LuCI web interface for nDPId management
- Add migration documentation from netifyd to nDPId
- Uses git dev branch for latest libndpi API compatibility
- Builds nDPId + nDPIsrvd event broker for microservice architecture

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-09 09:32:23 +01:00
parent 25385fc35d
commit e4a553a6d5
20 changed files with 3906 additions and 0 deletions

View File

@ -0,0 +1,682 @@
# Migration Plan: Netifyd to nDPId
## Executive Summary
This document provides a comprehensive migration plan to replace **Netifyd v5.2.1** with **nDPId** in the SecuBox OpenWrt project while maintaining full compatibility with existing CrowdSec and Netdata consumers.
**Key Finding**: Both Netifyd and nDPId are built on top of **nDPI** (the underlying DPI library). Netifyd is essentially a feature-rich wrapper around nDPI with cloud integration, while nDPId is a minimalist, high-performance daemon with a microservice architecture.
---
## Current Architecture Analysis
### Netifyd Integration Overview
| Component | Location | Purpose |
|-----------|----------|---------|
| Base Package | `secubox-app-netifyd` | Netifyd v5.2.1 DPI engine |
| LuCI App | `luci-app-secubox-netifyd` | Web UI with real-time monitoring |
| RPCD Backend | `/usr/libexec/rpcd/luci.secubox-netifyd` | 15 read + 9 write RPC methods |
| UCI Config | `/etc/config/secubox-netifyd` | Feature toggles, plugins, sinks |
| Status File | `/var/run/netifyd/status.json` | Summary statistics (NOT flows) |
| Socket | `/var/run/netifyd/netifyd.sock` | JSON streaming interface |
| Collector | `/usr/bin/netifyd-collector` | Periodic stats to `/tmp/netifyd-stats.json` |
### Current Data Consumers
1. **CrowdSec**: NO direct integration exists. Runs independently.
2. **Netdata**: Separate dashboard. Reads system metrics via `/proc`, not DPI data.
3. **LuCI Dashboard**: Primary consumer via RPCD backend.
### Netifyd Output Formats
**Summary Statistics** (`/var/run/netifyd/status.json`):
```json
{
"flow_count": 150,
"flows_active": 42,
"devices": [...],
"stats": {
"br-lan": {
"ip_bytes": 1234567,
"wire_bytes": 1345678,
"tcp": 1200,
"udp": 300,
"icmp": 50
}
},
"dns_hint_cache": { "cache_size": 500 },
"uptime": 86400
}
```
**Flow Data** (when sink enabled, not default):
```json
{
"flow_id": "abc123",
"src_ip": "192.168.1.100",
"dst_ip": "8.8.8.8",
"src_port": 54321,
"dst_port": 443,
"protocol": "tcp",
"application": "google",
"category": "search_engine",
"bytes_rx": 1500,
"bytes_tx": 500,
"packets_rx": 10,
"packets_tx": 5
}
```
---
## nDPId Architecture
### Core Components
| Component | Purpose |
|-----------|---------|
| **nDPId** | Traffic capture daemon using libpcap + libnDPI |
| **nDPIsrvd** | Broker that distributes events to multiple consumers |
| **libnDPI** | Core DPI library (shared with Netifyd) |
### nDPId Event System
**Message Format**: `[5-digit-length][JSON]\n`
```
01223{"flow_event_id":7,"flow_event_name":"detection-update",...}\n
```
**Event Categories**:
| Category | Events | Description |
|----------|--------|-------------|
| Error | 17 types | Packet processing failures, memory issues |
| Daemon | 4 types | init, shutdown, reconnect, status |
| Packet | 2 types | packet, packet-flow (base64 encoded) |
| Flow | 9 types | new, end, idle, update, detected, guessed, detection-update, not-detected, analyse |
### nDPId Flow Event Example
```json
{
"flow_event_id": 5,
"flow_event_name": "detected",
"thread_id": 0,
"packet_id": 12345,
"source": "eth0",
"flow_id": 1001,
"flow_state": "finished",
"flow_src_packets_processed": 15,
"flow_dst_packets_processed": 20,
"flow_first_seen": 1704067200000,
"flow_src_last_pkt_time": 1704067260000,
"flow_dst_last_pkt_time": 1704067258000,
"flow_idle_time": 2000,
"flow_src_tot_l4_payload_len": 1500,
"flow_dst_tot_l4_payload_len": 2000,
"l3_proto": "ip4",
"src_ip": "192.168.1.100",
"dst_ip": "142.250.185.78",
"l4_proto": "tcp",
"src_port": 54321,
"dst_port": 443,
"ndpi": {
"proto": "TLS.Google",
"proto_id": 91,
"proto_by_ip": 0,
"encrypted": 1,
"breed": "Safe",
"category_id": 5,
"category": "Web"
}
}
```
---
## Migration Strategy
### Phase 1: Compatibility Layer Development
Create a translation daemon that converts nDPId events to Netifyd-compatible format.
**New Component**: `secubox-ndpid-compat`
```
nDPId → nDPIsrvd → secubox-ndpid-compat → Existing Consumers
/var/run/netifyd/status.json (compatible)
/tmp/netifyd-stats.json (compatible)
RPCD backend (unchanged)
```
### Phase 2: Package Development
#### 2.1 New Package: `secubox-app-ndpid`
**Makefile**:
```makefile
PKG_NAME:=ndpid
PKG_VERSION:=1.7.0
PKG_RELEASE:=1
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/utoni/nDPId.git
DEPENDS:=+libndpi +libpcap +libjson-c +libpthread
```
**Build Requirements**:
- libnDPI ≥5.0.0
- libpcap
- libjson-c
- CMake build system
#### 2.2 New Package: `secubox-ndpid-compat`
Translation layer script that:
1. Connects to nDPIsrvd socket
2. Aggregates flow events into Netifyd-compatible format
3. Writes to `/var/run/netifyd/status.json`
4. Provides the same RPCD interface
### Phase 3: Output Format Translation
#### 3.1 Status File Translation Map
| Netifyd Field | nDPId Source | Translation Logic |
|---------------|--------------|-------------------|
| `flow_count` | Count of flow events | Increment on `new`, decrement on `end`/`idle` |
| `flows_active` | Active flow tracking | Count flows without `end`/`idle` events |
| `stats.{iface}.tcp` | `l4_proto == "tcp"` | Aggregate per interface |
| `stats.{iface}.udp` | `l4_proto == "udp"` | Aggregate per interface |
| `stats.{iface}.ip_bytes` | `flow_*_tot_l4_payload_len` | Sum per interface |
| `uptime` | Daemon `status` event | Direct mapping |
#### 3.2 Flow Data Translation Map
| Netifyd Field | nDPId Field | Notes |
|---------------|-------------|-------|
| `src_ip` | `src_ip` | Direct |
| `dst_ip` | `dst_ip` | Direct |
| `src_port` | `src_port` | Direct |
| `dst_port` | `dst_port` | Direct |
| `protocol` | `l4_proto` | Lowercase |
| `application` | `ndpi.proto` | Parse from "TLS.Google" → "google" |
| `category` | `ndpi.category` | Direct |
| `bytes_rx` | `flow_dst_tot_l4_payload_len` | Note: reversed (dst=rx from flow perspective) |
| `bytes_tx` | `flow_src_tot_l4_payload_len` | Note: reversed |
#### 3.3 Application Name Normalization
nDPId uses format like `TLS.Google`, `QUIC.YouTube`. Normalize to lowercase base:
```
TLS.Google → google
QUIC.YouTube → youtube
HTTP.Facebook → facebook
DNS → dns
```
### Phase 4: Consumer Compatibility
#### 4.1 CrowdSec Integration (NEW)
Since there's no existing CrowdSec integration, we can design it properly:
**Acquisition Configuration** (`/etc/crowdsec/acquis.d/ndpid.yaml`):
```yaml
source: file
filenames:
- /tmp/ndpid-flows.log
labels:
type: ndpid
---
source: journalctl
journalctl_filter:
- "_SYSTEMD_UNIT=ndpid.service"
labels:
type: syslog
```
**Parser** (`/etc/crowdsec/parsers/s02-enrich/ndpid-flows.yaml`):
```yaml
name: secubox/ndpid-flows
description: "Parse nDPId flow detection events"
filter: "evt.Parsed.program == 'ndpid'"
onsuccess: next_stage
statics:
- parsed: flow_application
expression: evt.Parsed.ndpi_proto
nodes:
- grok:
pattern: '%{IP:src_ip}:%{INT:src_port} -> %{IP:dst_ip}:%{INT:dst_port} %{WORD:proto} %{DATA:app}'
```
**Scenario** (`/etc/crowdsec/scenarios/ndpid-suspicious-app.yaml`):
```yaml
type: leaky
name: secubox/ndpid-suspicious-app
description: "Detect suspicious application usage"
filter: evt.Parsed.flow_application in ["bittorrent", "tor", "vpn_udp"]
groupby: evt.Parsed.src_ip
capacity: 5
leakspeed: 10m
blackhole: 1h
labels:
remediation: true
```
#### 4.2 Netdata Integration (NEW)
Create custom Netdata collector for nDPId:
**Collector** (`/usr/lib/netdata/plugins.d/ndpid.chart.sh`):
```bash
#!/bin/bash
# nDPId Netdata collector
NDPID_STATUS="/var/run/netifyd/status.json"
# Chart definitions
cat << EOF
CHART ndpid.flows '' "Network Flows" "flows" ndpid ndpid.flows area
DIMENSION active '' absolute 1 1
DIMENSION total '' absolute 1 1
EOF
while true; do
if [ -f "$NDPID_STATUS" ]; then
active=$(jq -r '.flows_active // 0' "$NDPID_STATUS")
total=$(jq -r '.flow_count // 0' "$NDPID_STATUS")
echo "BEGIN ndpid.flows"
echo "SET active = $active"
echo "SET total = $total"
echo "END"
fi
sleep 1
done
```
### Phase 5: Plugin System Migration
#### 5.1 IPSet Actions
Netifyd plugins → nDPId external processor:
| Netifyd Plugin | nDPId Equivalent |
|----------------|------------------|
| `libnetify-plugin-ipset.so` | External script consuming flow events |
| `libnetify-plugin-nftables.so` | External nftables updater |
**nDPId Flow Action Script** (`/usr/bin/ndpid-flow-actions`):
```bash
#!/bin/bash
# Process nDPId events and update ipsets
socat -u UNIX-RECV:/tmp/ndpid-actions.sock - | while read -r line; do
# Parse 5-digit length prefix
json="${line:5}"
event=$(echo "$json" | jq -r '.flow_event_name')
app=$(echo "$json" | jq -r '.ndpi.proto' | tr '.' '\n' | tail -1 | tr '[:upper:]' '[:lower:]')
case "$event" in
detected)
case "$app" in
bittorrent)
src_ip=$(echo "$json" | jq -r '.src_ip')
ipset add secubox-bittorrent "$src_ip" timeout 900 2>/dev/null
;;
esac
;;
esac
done
```
---
## Implementation Phases
### Phase 1: Foundation (Week 1-2)
1. [ ] Create `secubox-app-ndpid` package
2. [ ] Build nDPId + nDPIsrvd for OpenWrt
3. [ ] Test basic flow detection
4. [ ] Create UCI configuration schema
### Phase 2: Compatibility Layer (Week 3-4)
1. [ ] Develop `secubox-ndpid-compat` translation daemon
2. [ ] Implement status.json generation
3. [ ] Implement flow event aggregation
4. [ ] Test with existing LuCI dashboard
### Phase 3: RPCD Backend Update (Week 5)
1. [ ] Update RPCD methods to use nDPId data
2. [ ] Ensure all 15 read methods work
3. [ ] Ensure all 9 write methods work
4. [ ] Test LuCI application compatibility
### Phase 4: Consumer Integration (Week 6-7)
1. [ ] Create CrowdSec parser/scenario
2. [ ] Create Netdata collector
3. [ ] Test end-to-end data flow
4. [ ] Document new integrations
### Phase 5: Migration & Cleanup (Week 8)
1. [ ] Create migration script for existing users
2. [ ] Update documentation
3. [ ] Remove Netifyd package (optional, can coexist)
4. [ ] Final testing and release
---
## File Structure After Migration
```
package/secubox/
├── secubox-app-ndpid/ # NEW: nDPId package
│ ├── Makefile
│ ├── files/
│ │ ├── ndpid.config # UCI config
│ │ ├── ndpid.init # procd init script
│ │ └── ndpisrvd.init # nDPIsrvd init
│ └── patches/ # OpenWrt patches if needed
├── secubox-ndpid-compat/ # NEW: Compatibility layer
│ ├── Makefile
│ └── files/
│ ├── ndpid-compat.lua # Translation daemon
│ ├── ndpid-flow-actions # IPSet/nftables handler
│ └── ndpid-collector # Stats aggregator
├── luci-app-secubox-netifyd/ # MODIFIED: Works with both
│ └── root/usr/libexec/rpcd/
│ └── luci.secubox-netifyd # Updated for nDPId compat
└── secubox-app-netifyd/ # DEPRECATED: Keep for fallback
```
---
## Configuration Mapping
### UCI Config Translation
**Netifyd** (`/etc/config/secubox-netifyd`):
```
config settings 'settings'
option enabled '1'
option socket_type 'unix'
config sink 'sink'
option enabled '1'
option type 'unix'
option unix_path '/tmp/netifyd-flows.json'
```
**nDPId** (`/etc/config/secubox-ndpid`):
```
config ndpid 'main'
option enabled '1'
option interfaces 'br-lan br-wan'
option collector_socket '/tmp/ndpid-collector.sock'
config ndpisrvd 'distributor'
option enabled '1'
option listen_socket '/tmp/ndpisrvd.sock'
option tcp_port '7000'
config compat 'compat'
option enabled '1'
option netifyd_status '/var/run/netifyd/status.json'
option netifyd_socket '/var/run/netifyd/netifyd.sock'
```
---
## Risk Assessment
| Risk | Impact | Mitigation |
|------|--------|------------|
| Detection accuracy differences | Medium | Both use libnDPI; similar results expected |
| Performance regression | Low | nDPId is lighter; should improve performance |
| Plugin compatibility | High | Must reimplement flow actions externally |
| Breaking existing dashboards | High | Compatibility layer ensures same output format |
| Missing Netifyd features | Medium | Document feature gaps; prioritize critical ones |
### Features Comparison
| Feature | Netifyd | nDPId | Migration Impact |
|---------|---------|-------|------------------|
| Protocol detection | Yes | Yes | None |
| Application detection | Yes | Yes | None |
| Flow tracking | Yes | Yes | None |
| JSON output | Yes | Yes | Format translation needed |
| Socket streaming | Yes | Yes | Different format |
| Cloud integration | Yes | No | Feature removed |
| Plugin architecture | Built-in | External | Reimplement |
| Memory footprint | ~50MB | ~15MB | Improvement |
| Startup time | ~5s | ~1s | Improvement |
---
## Testing Plan
### Unit Tests
1. **Translation Accuracy**: Verify nDPId events correctly map to Netifyd format
2. **Statistics Aggregation**: Verify flow counts, bytes, packets match
3. **Application Detection**: Compare detection results between engines
### Integration Tests
1. **LuCI Dashboard**: All views render correctly
2. **RPCD Methods**: All 24 methods return expected data
3. **IPSet Actions**: BitTorrent/streaming detection triggers ipset updates
4. **CrowdSec Parsing**: Flow events parsed and scenarios trigger
### Performance Tests
1. **Throughput**: Measure max flows/second
2. **Memory**: Compare RAM usage under load
3. **CPU**: Compare CPU usage during traffic bursts
---
## Rollback Plan
If migration fails:
1. Stop nDPId services: `/etc/init.d/ndpid stop && /etc/init.d/ndpisrvd stop`
2. Start Netifyd: `/etc/init.d/netifyd start`
3. Compatibility layer auto-detects and switches source
4. No data loss; both can coexist
---
## References
- [nDPId GitHub Repository](https://github.com/utoni/nDPId)
- [nDPI Library](https://github.com/ntop/nDPI)
- [Netifyd Documentation](https://www.netify.ai/documentation/)
- [CrowdSec Acquisition](https://docs.crowdsec.net/docs/data_sources/intro)
- [Netdata External Plugins](https://learn.netdata.cloud/docs/agent/collectors/plugins.d)
---
## Appendix A: nDPId Event Schema Reference
### Flow Event Fields
```json
{
"flow_event_id": "integer (0-8)",
"flow_event_name": "string (new|end|idle|update|detected|guessed|detection-update|not-detected|analyse)",
"thread_id": "integer",
"packet_id": "integer",
"source": "string (interface name)",
"flow_id": "integer",
"flow_state": "string (skipped|finished|info)",
"l3_proto": "string (ip4|ip6)",
"src_ip": "string",
"dst_ip": "string",
"l4_proto": "string (tcp|udp|icmp|...)",
"src_port": "integer",
"dst_port": "integer",
"flow_src_packets_processed": "integer",
"flow_dst_packets_processed": "integer",
"flow_first_seen": "integer (ms timestamp)",
"flow_src_tot_l4_payload_len": "integer (bytes)",
"flow_dst_tot_l4_payload_len": "integer (bytes)",
"ndpi": {
"proto": "string (e.g., TLS.Google)",
"proto_id": "integer",
"encrypted": "integer (0|1)",
"breed": "string (Safe|Acceptable|Fun|Unsafe|...)",
"category_id": "integer",
"category": "string"
}
}
```
### Daemon Status Event Fields
```json
{
"daemon_event_id": 3,
"daemon_event_name": "status",
"global_ts_usec": "integer",
"uptime": "integer (seconds)",
"packets": "integer",
"packet_bytes": "integer",
"flows_active": "integer",
"flows_idle": "integer",
"flows_detected": "integer",
"compressions": "integer",
"decompressions": "integer"
}
```
---
## Appendix B: Sample Compatibility Layer Code
```lua
#!/usr/bin/env lua
-- secubox-ndpid-compat: nDPId to Netifyd format translator
local socket = require("socket")
local json = require("cjson")
local NDPISRVD_SOCK = "/tmp/ndpisrvd.sock"
local OUTPUT_STATUS = "/var/run/netifyd/status.json"
local UPDATE_INTERVAL = 1
-- State tracking
local state = {
flows = {},
flow_count = 0,
flows_active = 0,
stats = {},
devices = {},
uptime = 0,
start_time = os.time()
}
-- Process incoming nDPId event
local function process_event(raw)
-- Strip 5-digit length prefix
local json_str = raw:sub(6)
local ok, event = pcall(json.decode, json_str)
if not ok then return end
local event_name = event.flow_event_name or event.daemon_event_name
if event_name == "new" then
state.flows[event.flow_id] = event
state.flow_count = state.flow_count + 1
state.flows_active = state.flows_active + 1
elseif event_name == "end" or event_name == "idle" then
state.flows[event.flow_id] = nil
state.flows_active = state.flows_active - 1
elseif event_name == "detected" then
if state.flows[event.flow_id] then
state.flows[event.flow_id].detected = event.ndpi
end
-- Update interface stats
local iface = event.source or "unknown"
if not state.stats[iface] then
state.stats[iface] = {ip_bytes=0, tcp=0, udp=0, icmp=0}
end
local proto = event.l4_proto or ""
if proto == "tcp" then state.stats[iface].tcp = state.stats[iface].tcp + 1 end
if proto == "udp" then state.stats[iface].udp = state.stats[iface].udp + 1 end
if proto == "icmp" then state.stats[iface].icmp = state.stats[iface].icmp + 1 end
local bytes = (event.flow_src_tot_l4_payload_len or 0) + (event.flow_dst_tot_l4_payload_len or 0)
state.stats[iface].ip_bytes = state.stats[iface].ip_bytes + bytes
elseif event_name == "status" then
state.uptime = event.uptime or (os.time() - state.start_time)
end
end
-- Generate Netifyd-compatible status.json
local function generate_status()
return json.encode({
flow_count = state.flow_count,
flows_active = state.flows_active,
stats = state.stats,
devices = state.devices,
uptime = state.uptime,
dns_hint_cache = { cache_size = 0 }
})
end
-- Main loop
local function main()
-- Create output directory
os.execute("mkdir -p /var/run/netifyd")
local sock = socket.unix()
local ok, err = sock:connect(NDPISRVD_SOCK)
if not ok then
print("Failed to connect to nDPIsrvd: " .. (err or "unknown"))
os.exit(1)
end
sock:settimeout(0.1)
local last_write = 0
while true do
local line, err = sock:receive("*l")
if line then
process_event(line)
end
-- Write status file periodically
local now = os.time()
if now - last_write >= UPDATE_INTERVAL then
local f = io.open(OUTPUT_STATUS, "w")
if f then
f:write(generate_status())
f:close()
end
last_write = now
end
end
end
main()
```
---
*Document Version: 1.0*
*Created: 2026-01-09*
*Author: Claude Code Assistant*

View File

@ -0,0 +1,35 @@
# SPDX-License-Identifier: Apache-2.0
#
# Copyright (C) 2025 CyberMind.fr
#
# LuCI nDPId Dashboard - Deep Packet Inspection Interface
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-ndpid
PKG_VERSION:=1.0.0
PKG_RELEASE:=1
PKG_ARCH:=all
PKG_LICENSE:=Apache-2.0
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
LUCI_TITLE:=LuCI nDPId Dashboard
LUCI_DESCRIPTION:=Modern dashboard for nDPId deep packet inspection on OpenWrt
LUCI_DEPENDS:=+luci-base +luci-app-secubox +ndpid +socat +jq
LUCI_PKGARCH:=all
# File permissions
PKG_FILE_MODES:=/usr/libexec/rpcd/luci.ndpid:root:root:755 \
/usr/bin/ndpid-compat:root:root:755 \
/usr/bin/ndpid-flow-actions:root:root:755 \
/usr/bin/ndpid-collector:root:root:755
include $(TOPDIR)/feeds/luci/luci.mk
define Package/$(PKG_NAME)/conffiles
/etc/config/ndpid
endef
# call BuildPackage - OpenWrt buildroot

View File

@ -0,0 +1,207 @@
'use strict';
'require rpc';
var callServiceStatus = rpc.declare({
object: 'luci.ndpid',
method: 'get_service_status',
expect: { }
});
var callRealtimeFlows = rpc.declare({
object: 'luci.ndpid',
method: 'get_realtime_flows',
expect: { }
});
var callInterfaceStats = rpc.declare({
object: 'luci.ndpid',
method: 'get_interface_stats',
expect: { interfaces: [] }
});
var callTopApplications = rpc.declare({
object: 'luci.ndpid',
method: 'get_top_applications',
expect: { applications: [] }
});
var callTopProtocols = rpc.declare({
object: 'luci.ndpid',
method: 'get_top_protocols',
expect: { protocols: [] }
});
var callConfig = rpc.declare({
object: 'luci.ndpid',
method: 'get_config',
expect: { }
});
var callDashboard = rpc.declare({
object: 'luci.ndpid',
method: 'get_dashboard',
expect: { }
});
var callInterfaces = rpc.declare({
object: 'luci.ndpid',
method: 'get_interfaces',
expect: { interfaces: [], available: [] }
});
var callServiceStart = rpc.declare({
object: 'luci.ndpid',
method: 'service_start',
expect: { success: false }
});
var callServiceStop = rpc.declare({
object: 'luci.ndpid',
method: 'service_stop',
expect: { success: false }
});
var callServiceRestart = rpc.declare({
object: 'luci.ndpid',
method: 'service_restart',
expect: { success: false }
});
var callServiceEnable = rpc.declare({
object: 'luci.ndpid',
method: 'service_enable',
expect: { success: false }
});
var callServiceDisable = rpc.declare({
object: 'luci.ndpid',
method: 'service_disable',
expect: { success: false }
});
var callUpdateConfig = rpc.declare({
object: 'luci.ndpid',
method: 'update_config',
params: ['data'],
expect: { success: false }
});
var callClearCache = rpc.declare({
object: 'luci.ndpid',
method: 'clear_cache',
expect: { success: false }
});
return {
// Read methods
getServiceStatus: function() {
return callServiceStatus();
},
getRealtimeFlows: function() {
return callRealtimeFlows();
},
getInterfaceStats: function() {
return callInterfaceStats();
},
getTopApplications: function() {
return callTopApplications();
},
getTopProtocols: function() {
return callTopProtocols();
},
getConfig: function() {
return callConfig();
},
getDashboard: function() {
return callDashboard();
},
getInterfaces: function() {
return callInterfaces();
},
getAllData: function() {
return Promise.all([
callDashboard(),
callInterfaceStats(),
callTopApplications(),
callTopProtocols()
]).then(function(results) {
return {
dashboard: results[0],
interfaces: results[1],
applications: results[2],
protocols: results[3]
};
});
},
// Write methods
serviceStart: function() {
return callServiceStart();
},
serviceStop: function() {
return callServiceStop();
},
serviceRestart: function() {
return callServiceRestart();
},
serviceEnable: function() {
return callServiceEnable();
},
serviceDisable: function() {
return callServiceDisable();
},
updateConfig: function(data) {
return callUpdateConfig(data);
},
clearCache: function() {
return callClearCache();
},
// Utility functions
formatBytes: function(bytes) {
if (bytes === 0 || bytes === null || bytes === undefined) return '0 B';
var k = 1024;
var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
var i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
},
formatNumber: function(num) {
if (num === null || num === undefined) return '0';
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
},
formatUptime: function(seconds) {
if (!seconds || seconds === 0) return 'Not running';
var days = Math.floor(seconds / 86400);
var hours = Math.floor((seconds % 86400) / 3600);
var minutes = Math.floor((seconds % 3600) / 60);
var parts = [];
if (days > 0) parts.push(days + 'd');
if (hours > 0) parts.push(hours + 'h');
if (minutes > 0) parts.push(minutes + 'm');
return parts.length > 0 ? parts.join(' ') : '< 1m';
},
getStatusClass: function(running) {
return running ? 'active' : 'inactive';
},
getStatusText: function(running) {
return running ? 'Running' : 'Stopped';
}
};

View File

@ -0,0 +1,529 @@
/* nDPId Dashboard Styles
* Copyright (C) 2025 CyberMind.fr
*/
:root {
--ndpi-bg-primary: #030712;
--ndpi-bg-secondary: #0f172a;
--ndpi-bg-tertiary: #1e293b;
--ndpi-border: #334155;
--ndpi-text-primary: #f8fafc;
--ndpi-text-secondary: #94a3b8;
--ndpi-text-muted: #64748b;
--ndpi-accent-cyan: #06b6d4;
--ndpi-accent-blue: #0ea5e9;
--ndpi-accent-green: #10b981;
--ndpi-accent-yellow: #f59e0b;
--ndpi-accent-red: #ef4444;
--ndpi-gradient: linear-gradient(135deg, #06b6d4, #0ea5e9, #6366f1);
--ndpi-font-mono: 'JetBrains Mono', 'Fira Code', monospace;
--ndpi-font-sans: 'Inter', -apple-system, sans-serif;
--ndpi-radius: 8px;
--ndpi-radius-lg: 12px;
}
.ndpid-dashboard {
font-family: var(--ndpi-font-sans);
background: var(--ndpi-bg-primary);
color: var(--ndpi-text-primary);
min-height: 100vh;
padding: 16px;
}
.ndpid-dashboard * {
box-sizing: border-box;
}
/* Header */
.ndpi-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0 20px;
border-bottom: 1px solid var(--ndpi-border);
margin-bottom: 20px;
}
.ndpi-logo {
display: flex;
align-items: center;
gap: 14px;
}
.ndpi-logo-icon {
width: 46px;
height: 46px;
background: var(--ndpi-gradient);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
box-shadow: 0 0 30px rgba(6, 182, 212, 0.4);
}
.ndpi-logo-text {
font-size: 24px;
font-weight: 700;
}
.ndpi-logo-text span {
background: var(--ndpi-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.ndpi-header-info {
display: flex;
align-items: center;
gap: 16px;
}
.ndpi-status-badge {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 600;
padding: 8px 16px;
border-radius: 24px;
}
.ndpi-status-badge.running {
background: rgba(16, 185, 129, 0.15);
color: var(--ndpi-accent-green);
border: 1px solid rgba(16, 185, 129, 0.3);
}
.ndpi-status-badge.stopped {
background: rgba(239, 68, 68, 0.15);
color: var(--ndpi-accent-red);
border: 1px solid rgba(239, 68, 68, 0.3);
}
.ndpi-status-dot {
width: 10px;
height: 10px;
background: currentColor;
border-radius: 50%;
animation: ndpi-pulse 1.5s ease-in-out infinite;
}
.ndpi-version {
font-size: 12px;
color: var(--ndpi-text-muted);
font-family: var(--ndpi-font-mono);
}
@keyframes ndpi-pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.6; transform: scale(0.85); }
}
/* Controls */
.ndpi-controls {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
padding: 12px 16px;
background: var(--ndpi-bg-secondary);
border: 1px solid var(--ndpi-border);
border-radius: var(--ndpi-radius);
}
.ndpi-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border: 1px solid var(--ndpi-border);
border-radius: var(--ndpi-radius);
background: var(--ndpi-bg-tertiary);
color: var(--ndpi-text-primary);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.ndpi-btn:hover:not(:disabled) {
border-color: var(--ndpi-accent-cyan);
}
.ndpi-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.ndpi-btn-primary {
background: var(--ndpi-gradient);
border: none;
color: white;
}
.ndpi-btn-success {
background: rgba(16, 185, 129, 0.2);
border-color: var(--ndpi-accent-green);
color: var(--ndpi-accent-green);
}
.ndpi-btn-danger {
background: rgba(239, 68, 68, 0.2);
border-color: var(--ndpi-accent-red);
color: var(--ndpi-accent-red);
}
.ndpi-btn-sm {
padding: 6px 12px;
font-size: 12px;
}
.ndpi-refresh-status {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--ndpi-text-secondary);
}
.ndpi-refresh-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--ndpi-text-muted);
}
.ndpi-refresh-dot.active {
background: var(--ndpi-accent-green);
animation: ndpi-pulse 1.5s ease-in-out infinite;
}
/* Quick Stats */
.ndpi-quick-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 14px;
margin-bottom: 24px;
}
.ndpi-quick-stat {
background: var(--ndpi-bg-secondary);
border: 1px solid var(--ndpi-border);
border-radius: var(--ndpi-radius-lg);
padding: 20px;
position: relative;
overflow: hidden;
transition: all 0.3s;
}
.ndpi-quick-stat::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: var(--stat-gradient, var(--ndpi-gradient));
}
.ndpi-quick-stat:hover {
border-color: var(--ndpi-accent-cyan);
transform: translateY(-3px);
}
.ndpi-quick-stat-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}
.ndpi-quick-stat-icon {
font-size: 22px;
}
.ndpi-quick-stat-label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--ndpi-text-muted);
}
.ndpi-quick-stat-value {
font-family: var(--ndpi-font-mono);
font-size: 32px;
font-weight: 700;
line-height: 1;
background: var(--stat-gradient, var(--ndpi-gradient));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.ndpi-quick-stat-sub {
font-size: 11px;
color: var(--ndpi-text-muted);
margin-top: 6px;
}
/* Card */
.ndpi-card {
background: var(--ndpi-bg-secondary);
border: 1px solid var(--ndpi-border);
border-radius: var(--ndpi-radius-lg);
overflow: hidden;
margin-bottom: 20px;
}
.ndpi-card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--ndpi-border);
background: rgba(0, 0, 0, 0.3);
}
.ndpi-card-title {
display: flex;
align-items: center;
gap: 12px;
font-size: 15px;
font-weight: 600;
}
.ndpi-card-title-icon {
font-size: 20px;
}
.ndpi-card-badge {
font-family: var(--ndpi-font-mono);
font-size: 12px;
font-weight: 600;
padding: 5px 12px;
border-radius: 16px;
background: var(--ndpi-gradient);
color: white;
}
.ndpi-card-body {
padding: 20px;
}
/* Interface Grid */
.ndpi-iface-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 16px;
}
.ndpi-iface-card {
background: var(--ndpi-bg-tertiary);
border: 1px solid var(--ndpi-border);
border-radius: var(--ndpi-radius);
padding: 16px;
}
.ndpi-iface-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.ndpi-iface-icon {
font-size: 24px;
}
.ndpi-iface-name {
font-size: 16px;
font-weight: 600;
}
.ndpi-iface-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.ndpi-iface-stat {
text-align: center;
padding: 8px;
background: var(--ndpi-bg-primary);
border-radius: var(--ndpi-radius);
}
.ndpi-iface-stat-label {
display: block;
font-size: 10px;
text-transform: uppercase;
color: var(--ndpi-text-muted);
margin-bottom: 4px;
}
.ndpi-iface-stat-value {
font-family: var(--ndpi-font-mono);
font-size: 14px;
font-weight: 600;
color: var(--ndpi-accent-cyan);
}
/* Table */
.ndpi-table-container {
overflow-x: auto;
}
.ndpi-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.ndpi-table th {
text-align: left;
padding: 12px 16px;
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);
}
.ndpi-table td {
padding: 12px 16px;
border-bottom: 1px solid var(--ndpi-border);
}
.ndpi-table tr:last-child td {
border-bottom: none;
}
.ndpi-table tr:hover td {
background: rgba(6, 182, 212, 0.05);
}
.ndpi-table .mono {
font-family: var(--ndpi-font-mono);
font-size: 12px;
}
.ndpi-app-name {
font-weight: 500;
color: var(--ndpi-accent-cyan);
}
/* Protocol Distribution */
.ndpi-protocol-grid {
display: flex;
flex-direction: column;
gap: 12px;
}
.ndpi-protocol-item {
display: grid;
grid-template-columns: 80px 1fr 50px;
align-items: center;
gap: 12px;
}
.ndpi-protocol-name {
font-weight: 600;
font-size: 14px;
}
.ndpi-protocol-count {
font-family: var(--ndpi-font-mono);
font-size: 12px;
color: var(--ndpi-text-secondary);
}
.ndpi-protocol-bar {
height: 8px;
background: var(--ndpi-bg-primary);
border-radius: 4px;
overflow: hidden;
}
.ndpi-protocol-bar-fill {
height: 100%;
background: var(--ndpi-gradient);
transition: width 0.5s ease;
}
.ndpi-protocol-pct {
font-family: var(--ndpi-font-mono);
font-size: 12px;
text-align: right;
color: var(--ndpi-text-muted);
}
/* Empty State */
.ndpi-empty {
text-align: center;
padding: 60px 20px;
color: var(--ndpi-text-muted);
}
.ndpi-empty-icon {
font-size: 64px;
margin-bottom: 16px;
opacity: 0.5;
}
.ndpi-empty-text {
font-size: 16px;
margin-bottom: 8px;
}
/* Value Update Animation */
@keyframes ndpi-value-flash {
0% { background-color: transparent; }
50% { background-color: rgba(6, 182, 212, 0.3); }
100% { background-color: transparent; }
}
.ndpi-value-updated {
animation: ndpi-value-flash 0.5s ease-out;
border-radius: 4px;
}
/* Responsive */
@media (max-width: 768px) {
.ndpi-header {
flex-direction: column;
gap: 16px;
align-items: flex-start;
}
.ndpi-controls {
flex-wrap: wrap;
}
.ndpi-quick-stats {
grid-template-columns: repeat(2, 1fr);
}
.ndpi-quick-stat-value {
font-size: 24px;
}
.ndpi-iface-grid {
grid-template-columns: 1fr;
}
}
/* Scrollbar */
.ndpid-dashboard ::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.ndpid-dashboard ::-webkit-scrollbar-track {
background: var(--ndpi-bg-tertiary);
}
.ndpid-dashboard ::-webkit-scrollbar-thumb {
background: var(--ndpi-border);
border-radius: 4px;
}

View File

@ -0,0 +1,364 @@
'use strict';
'require view';
'require poll';
'require dom';
'require ui';
'require ndpid.api as api';
return view.extend({
title: _('nDPId Dashboard'),
pollInterval: 5,
pollActive: true,
load: function() {
return api.getAllData();
},
updateDashboard: function(data) {
var dashboard = data.dashboard || {};
var service = dashboard.service || {};
var flows = dashboard.flows || {};
var system = dashboard.system || {};
// Update service status
var statusBadge = document.querySelector('.ndpi-status-badge');
if (statusBadge) {
statusBadge.classList.toggle('running', service.running);
statusBadge.classList.toggle('stopped', !service.running);
statusBadge.innerHTML = '<span class="ndpi-status-dot"></span>' +
(service.running ? 'Running' : 'Stopped');
}
// Update flow counts
var updates = [
{ sel: '.ndpi-stat-flows-total', val: api.formatNumber(flows.total) },
{ sel: '.ndpi-stat-flows-active', val: api.formatNumber(flows.active) },
{ sel: '.ndpi-stat-memory', val: api.formatBytes(system.memory_kb * 1024) }
];
updates.forEach(function(u) {
var el = document.querySelector(u.sel);
if (el && el.textContent !== u.val) {
el.textContent = u.val;
el.classList.add('ndpi-value-updated');
setTimeout(function() { el.classList.remove('ndpi-value-updated'); }, 500);
}
});
// Update interface stats
var interfaces = (data.interfaces || {}).interfaces || [];
interfaces.forEach(function(iface) {
var card = document.querySelector('.ndpi-iface-card[data-iface="' + iface.name + '"]');
if (!card) return;
var tcpEl = card.querySelector('.ndpi-iface-tcp');
var udpEl = card.querySelector('.ndpi-iface-udp');
var bytesEl = card.querySelector('.ndpi-iface-bytes');
if (tcpEl) tcpEl.textContent = api.formatNumber(iface.tcp);
if (udpEl) udpEl.textContent = api.formatNumber(iface.udp);
if (bytesEl) bytesEl.textContent = api.formatBytes(iface.ip_bytes);
});
},
startPolling: function() {
var self = this;
this.pollActive = true;
poll.add(L.bind(function() {
if (!this.pollActive) return Promise.resolve();
return api.getAllData().then(L.bind(function(data) {
this.updateDashboard(data);
}, this));
}, this), this.pollInterval);
},
stopPolling: function() {
this.pollActive = false;
poll.stop();
},
handleServiceControl: function(action) {
var self = this;
ui.showModal(_('Please wait...'), [
E('p', { 'class': 'spinning' }, _('Processing request...'))
]);
var promise;
switch (action) {
case 'start':
promise = api.serviceStart();
break;
case 'stop':
promise = api.serviceStop();
break;
case 'restart':
promise = api.serviceRestart();
break;
default:
ui.hideModal();
return;
}
promise.then(function(result) {
ui.hideModal();
if (result.success) {
ui.addNotification(null, E('p', {}, result.message || _('Operation completed')), 'info');
} else {
ui.addNotification(null, E('p', {}, result.message || _('Operation failed')), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', {}, _('Error: ') + err.message), 'error');
});
},
render: function(data) {
var self = this;
var dashboard = data.dashboard || {};
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 || [];
var view = E('div', { 'class': 'ndpid-dashboard' }, [
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' }, ['nDPI', E('span', {}, 'd')])
]),
E('div', { 'class': 'ndpi-header-info' }, [
E('div', {
'class': 'ndpi-status-badge ' + (service.running ? 'running' : 'stopped')
}, [
E('span', { 'class': 'ndpi-status-dot' }),
service.running ? 'Running' : 'Stopped'
]),
E('span', { 'class': 'ndpi-version' }, 'v' + (service.version || '1.7'))
])
]),
// Service controls
E('div', { 'class': 'ndpi-controls' }, [
E('button', {
'class': 'ndpi-btn ndpi-btn-success',
'click': function() { self.handleServiceControl('start'); },
'disabled': service.running
}, '▶ Start'),
E('button', {
'class': 'ndpi-btn ndpi-btn-danger',
'click': function() { self.handleServiceControl('stop'); },
'disabled': !service.running
}, '⏹ Stop'),
E('button', {
'class': 'ndpi-btn ndpi-btn-primary',
'click': function() { self.handleServiceControl('restart'); }
}, '🔄 Restart'),
E('div', { 'style': 'flex: 1' }),
E('span', { 'class': 'ndpi-refresh-status' }, [
E('span', { 'class': 'ndpi-refresh-dot active' }),
' Auto-refresh: ',
E('span', { 'class': 'ndpi-refresh-state' }, 'Active')
]),
E('button', {
'class': 'ndpi-btn ndpi-btn-sm',
'id': 'ndpi-poll-toggle',
'click': L.bind(function(ev) {
var btn = ev.target;
var indicator = document.querySelector('.ndpi-refresh-dot');
var state = document.querySelector('.ndpi-refresh-state');
if (this.pollActive) {
this.stopPolling();
btn.textContent = '▶ Resume';
indicator.classList.remove('active');
state.textContent = 'Paused';
} else {
this.startPolling();
btn.textContent = '⏸ Pause';
indicator.classList.add('active');
state.textContent = 'Active';
}
}, this)
}, '⏸ Pause')
]),
// Quick Stats
E('div', { 'class': 'ndpi-quick-stats' }, [
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 Flows')
]),
E('div', { 'class': 'ndpi-quick-stat-value ndpi-stat-flows-total' },
api.formatNumber(flows.total || 0)),
E('div', { 'class': 'ndpi-quick-stat-sub' }, 'Detected since start')
]),
E('div', { 'class': 'ndpi-quick-stat', 'style': '--stat-gradient: linear-gradient(135deg, #10b981, #34d399)' }, [
E('div', { 'class': 'ndpi-quick-stat-header' }, [
E('span', { 'class': 'ndpi-quick-stat-icon' }, '✅'),
E('span', { 'class': 'ndpi-quick-stat-label' }, 'Active Flows')
]),
E('div', { 'class': 'ndpi-quick-stat-value ndpi-stat-flows-active' },
api.formatNumber(flows.active || 0)),
E('div', { 'class': 'ndpi-quick-stat-sub' }, 'Currently tracked')
]),
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' }, 'Memory')
]),
E('div', { 'class': 'ndpi-quick-stat-value ndpi-stat-memory' },
api.formatBytes((system.memory_kb || 0) * 1024)),
E('div', { 'class': 'ndpi-quick-stat-sub' }, 'Process memory')
]),
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' }, 'Interfaces')
]),
E('div', { 'class': 'ndpi-quick-stat-value' },
(dashboard.interfaces || []).length),
E('div', { 'class': 'ndpi-quick-stat-sub' }, 'Monitored')
])
]),
// Interface Statistics
E('div', { 'class': 'ndpi-card' }, [
E('div', { 'class': 'ndpi-card-header' }, [
E('div', { 'class': 'ndpi-card-title' }, [
E('span', { 'class': 'ndpi-card-title-icon' }, '🔗'),
'Interface Statistics'
]),
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-iface-grid' },
interfaces.map(function(iface) {
return E('div', { 'class': 'ndpi-iface-card', 'data-iface': iface.name }, [
E('div', { 'class': 'ndpi-iface-header' }, [
E('div', { 'class': 'ndpi-iface-icon' }, '🌐'),
E('div', { 'class': 'ndpi-iface-name' }, iface.name)
]),
E('div', { 'class': 'ndpi-iface-stats' }, [
E('div', { 'class': 'ndpi-iface-stat' }, [
E('span', { 'class': 'ndpi-iface-stat-label' }, 'TCP'),
E('span', { 'class': 'ndpi-iface-stat-value ndpi-iface-tcp' },
api.formatNumber(iface.tcp))
]),
E('div', { 'class': 'ndpi-iface-stat' }, [
E('span', { 'class': 'ndpi-iface-stat-label' }, 'UDP'),
E('span', { 'class': 'ndpi-iface-stat-value ndpi-iface-udp' },
api.formatNumber(iface.udp))
]),
E('div', { 'class': 'ndpi-iface-stat' }, [
E('span', { 'class': 'ndpi-iface-stat-label' }, 'Bytes'),
E('span', { 'class': 'ndpi-iface-stat-value ndpi-iface-bytes' },
api.formatBytes(iface.ip_bytes))
])
])
]);
})
) :
E('div', { 'class': 'ndpi-empty' }, [
E('div', { 'class': 'ndpi-empty-icon' }, '📡'),
E('div', { 'class': 'ndpi-empty-text' }, 'No interface statistics available'),
E('p', {}, 'Start the nDPId service to begin monitoring')
])
)
]),
// 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')
]),
E('td', { 'class': 'mono' }, api.formatNumber(app.flows)),
E('td', { 'class': 'mono' }, api.formatBytes(app.bytes))
]);
})
)
])
]) :
E('div', { 'class': 'ndpi-empty' }, [
E('div', { 'class': 'ndpi-empty-icon' }, '📱'),
E('div', { 'class': 'ndpi-empty-text' }, 'No applications detected yet')
])
)
]),
// Top Protocols
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 total = protocols.reduce(function(sum, p) { return sum + (p.count || 0); }, 0);
var pct = total > 0 ? Math.round((proto.count / total) * 100) : 0;
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 + '%'
})
]),
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')
])
)
])
]);
// Start polling
this.startPolling();
return view;
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});

View File

@ -0,0 +1,239 @@
'use strict';
'require view';
'require poll';
'require dom';
'require ui';
'require ndpid.api as api';
return view.extend({
title: _('nDPId Flows'),
pollInterval: 3,
pollActive: true,
load: function() {
return Promise.all([
api.getRealtimeFlows(),
api.getInterfaceStats(),
api.getTopProtocols()
]).then(function(results) {
return {
flows: results[0],
interfaces: results[1],
protocols: results[2]
};
});
},
updateFlows: function(data) {
var flows = data.flows || {};
// Update flow counts
var activeEl = document.querySelector('.ndpi-flows-active');
var totalEl = document.querySelector('.ndpi-flows-total');
if (activeEl) {
var newActive = api.formatNumber(flows.flows_active || 0);
if (activeEl.textContent !== newActive) {
activeEl.textContent = newActive;
activeEl.classList.add('ndpi-value-updated');
setTimeout(function() { activeEl.classList.remove('ndpi-value-updated'); }, 500);
}
}
if (totalEl) {
var newTotal = api.formatNumber(flows.flow_count || 0);
if (totalEl.textContent !== newTotal) {
totalEl.textContent = newTotal;
}
}
// 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;
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);
});
},
startPolling: function() {
var self = this;
this.pollActive = true;
poll.add(L.bind(function() {
if (!this.pollActive) return Promise.resolve();
return Promise.all([
api.getRealtimeFlows(),
api.getInterfaceStats()
]).then(L.bind(function(results) {
this.updateFlows({
flows: results[0],
interfaces: results[1]
});
}, this));
}, this), this.pollInterval);
},
stopPolling: function() {
this.pollActive = false;
poll.stop();
},
render: function(data) {
var self = this;
var flows = data.flows || {};
var interfaces = (data.interfaces || {}).interfaces || [];
var protocols = (data.protocols || {}).protocols || [];
// Calculate protocol totals
var totalPackets = protocols.reduce(function(sum, p) { return sum + (p.count || 0); }, 0);
var view = E('div', { 'class': 'ndpid-dashboard' }, [
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')])
])
]),
// Flow Summary
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' }, [
E('span', { 'class': 'ndpi-quick-stat-icon' }, '✅'),
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')
]),
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 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')
]),
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('div', { 'class': 'ndpi-quick-stat-value' },
api.formatNumber(totalPackets)),
E('div', { 'class': 'ndpi-quick-stat-sub' }, 'TCP + UDP + ICMP')
]),
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('div', { 'class': 'ndpi-quick-stat-value' },
api.formatUptime(flows.uptime || 0)),
E('div', { 'class': 'ndpi-quick-stat-sub' }, 'Service runtime')
])
]),
// 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'
]),
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('thead', {}, [
E('tr', {}, [
E('th', {}, 'Interface'),
E('th', {}, 'TCP'),
E('th', {}, 'UDP'),
E('th', {}, 'ICMP'),
E('th', {}, 'Total Bytes')
])
]),
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('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('div', { 'class': 'ndpi-empty' }, [
E('div', { 'class': 'ndpi-empty-icon' }, '📊'),
E('div', { 'class': 'ndpi-empty-text' }, 'No interface statistics available')
])
)
]),
// 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'
])
]),
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')
])
)
])
]);
// Start polling
this.startPolling();
return view;
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});

View File

@ -0,0 +1,165 @@
'use strict';
'require view';
'require form';
'require uci';
'require ui';
'require ndpid.api as api';
return view.extend({
title: _('nDPId Settings'),
load: function() {
return Promise.all([
uci.load('ndpid'),
api.getInterfaces()
]);
},
render: function(data) {
var interfaces = data[1] || {};
var available = interfaces.available || [];
var m, s, o;
m = new form.Map('ndpid', _('nDPId Configuration'),
_('Configure the nDPId deep packet inspection daemon.'));
// Main Settings
s = m.section(form.TypedSection, 'ndpid', _('Service Settings'));
s.anonymous = true;
o = s.option(form.Flag, 'enabled', _('Enable nDPId'),
_('Start nDPId service on boot'));
o.default = '0';
o.rmempty = false;
o = s.option(form.MultiValue, 'interface', _('Monitored Interfaces'),
_('Select network interfaces to monitor for traffic'));
available.forEach(function(iface) {
o.value(iface, iface);
});
o.default = 'br-lan';
o = s.option(form.Value, 'max_flows', _('Maximum Flows'),
_('Maximum number of concurrent flows to track'));
o.datatype = 'uinteger';
o.default = '100000';
o.placeholder = '100000';
o = s.option(form.Value, 'collector_socket', _('Collector Socket'),
_('Path to the collector socket'));
o.default = '/var/run/ndpid/collector.sock';
o.placeholder = '/var/run/ndpid/collector.sock';
o = s.option(form.Value, 'flow_idle_timeout', _('Flow Idle Timeout (ms)'),
_('Time before idle flows are expired'));
o.datatype = 'uinteger';
o.default = '600000';
o.placeholder = '600000';
o = s.option(form.Value, 'tcp_timeout', _('TCP Timeout (ms)'),
_('Timeout for TCP connections'));
o.datatype = 'uinteger';
o.default = '7200000';
o.placeholder = '7200000';
o = s.option(form.Value, 'udp_timeout', _('UDP Timeout (ms)'),
_('Timeout for UDP flows'));
o.datatype = 'uinteger';
o.default = '180000';
o.placeholder = '180000';
o = s.option(form.Flag, 'compression', _('Enable Compression'),
_('Compress data sent to distributor'));
o.default = '1';
// Distributor Settings
s = m.section(form.TypedSection, 'ndpisrvd', _('Distributor Settings'),
_('Configure the nDPIsrvd event distributor'));
s.anonymous = true;
o = s.option(form.Flag, 'enabled', _('Enable Distributor'),
_('Enable nDPIsrvd event distribution'));
o.default = '1';
o.rmempty = false;
o = s.option(form.Value, 'listen_socket', _('Listen Socket'),
_('Unix socket path for clients'));
o.default = '/var/run/ndpid/distributor.sock';
o = s.option(form.Value, 'tcp_port', _('TCP Port'),
_('TCP port for remote clients (0 to disable)'));
o.datatype = 'port';
o.default = '7000';
o.placeholder = '7000';
o = s.option(form.Value, 'tcp_address', _('TCP Address'),
_('Address to bind TCP listener'));
o.default = '127.0.0.1';
o.placeholder = '127.0.0.1';
o = s.option(form.Value, 'max_clients', _('Max Clients'),
_('Maximum number of connected clients'));
o.datatype = 'uinteger';
o.default = '10';
// Compatibility Layer
s = m.section(form.TypedSection, 'compat', _('Compatibility Layer'),
_('Netifyd-compatible output for existing consumers'));
s.anonymous = true;
o = s.option(form.Flag, 'enabled', _('Enable Compatibility'),
_('Generate Netifyd-compatible status files'));
o.default = '1';
o.rmempty = false;
o = s.option(form.Value, 'status_file', _('Status File'),
_('Path for Netifyd-compatible status.json'));
o.default = '/var/run/netifyd/status.json';
o = s.option(form.Value, 'update_interval', _('Update Interval'),
_('How often to update status file (seconds)'));
o.datatype = 'uinteger';
o.default = '1';
// Flow Actions
s = m.section(form.TypedSection, 'actions', _('Flow Actions'),
_('Automatic actions based on detected applications'));
s.anonymous = true;
o = s.option(form.Flag, 'enabled', _('Enable Flow Actions'),
_('Process flow events and update ipsets'));
o.default = '0';
o.rmempty = false;
o = s.option(form.Value, 'bittorrent_ipset', _('BitTorrent IPSet'),
_('IPSet for BitTorrent traffic'));
o.default = 'secubox-bittorrent';
o.depends('enabled', '1');
o = s.option(form.Value, 'bittorrent_timeout', _('BitTorrent Timeout'),
_('IPSet entry timeout in seconds'));
o.datatype = 'uinteger';
o.default = '900';
o.depends('enabled', '1');
o = s.option(form.Value, 'streaming_ipset', _('Streaming IPSet'),
_('IPSet for streaming services'));
o.default = 'secubox-streaming';
o.depends('enabled', '1');
o = s.option(form.Value, 'blocked_ipset', _('Blocked IPSet'),
_('IPSet for blocked applications'));
o.default = 'secubox-blocked';
o.depends('enabled', '1');
o = s.option(form.DynamicList, 'blocked_app', _('Blocked Applications'),
_('Applications to block'));
o.value('bittorrent', 'BitTorrent');
o.value('tor', 'Tor');
o.value('vpn_udp', 'VPN (UDP)');
o.depends('enabled', '1');
return m.render();
}
});

View File

@ -0,0 +1,42 @@
#!/bin/sh /etc/rc.common
# nDPId Compatibility Layer init script
# Copyright (C) 2025 CyberMind.fr
START=52
STOP=9
USE_PROCD=1
PROG=/usr/bin/ndpid-compat
start_service() {
local enabled
config_load ndpid
config_get_bool enabled compat enabled 1
[ "$enabled" -eq 0 ] && {
logger -t ndpid-compat "Compatibility layer disabled"
return 0
}
# Wait for nDPIsrvd
sleep 2
procd_open_instance ndpid-compat
procd_set_param command "$PROG"
procd_set_param respawn ${respawn_threshold:-3600} ${respawn_timeout:-5} ${respawn_retry:-5}
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param pidfile /var/run/ndpid-compat.pid
procd_close_instance
logger -t ndpid-compat "Started compatibility layer"
}
stop_service() {
logger -t ndpid-compat "Stopping compatibility layer"
}
service_triggers() {
procd_add_reload_trigger "ndpid"
}

View File

@ -0,0 +1,112 @@
#!/bin/sh
# nDPId Statistics Collector
# Collects and aggregates DPI statistics for dashboards
# Copyright (C) 2025 CyberMind.fr
STATUS_FILE="/var/run/netifyd/status.json"
STATS_FILE="/tmp/ndpid-stats.json"
HISTORY_FILE="/tmp/ndpid-stats-history.json"
FLOWS_FILE="/tmp/ndpid-flows.json"
MAX_HISTORY=1440 # 24 hours at 1-minute intervals
# Collect current statistics
collect_stats() {
local timestamp=$(date +%s)
if [ ! -f "$STATUS_FILE" ]; then
echo '{"error": "Status file not found", "timestamp": '$timestamp'}'
return 1
fi
# Read current status
local status=$(cat "$STATUS_FILE" 2>/dev/null)
[ -z "$status" ] && return 1
# Extract values
local flow_count=$(echo "$status" | jsonfilter -e '@.flow_count' 2>/dev/null || echo 0)
local flows_active=$(echo "$status" | jsonfilter -e '@.flows_active' 2>/dev/null || echo 0)
local uptime=$(echo "$status" | jsonfilter -e '@.uptime' 2>/dev/null || echo 0)
# Get interface stats
local stats=""
if command -v jq >/dev/null 2>&1; then
stats=$(echo "$status" | jq -c '.stats // {}')
else
stats="{}"
fi
# Create snapshot
cat << EOF
{
"timestamp": $timestamp,
"flow_count": $flow_count,
"flows_active": $flows_active,
"uptime": $uptime,
"stats": $stats
}
EOF
}
# Add to history
add_to_history() {
local snapshot="$1"
# Initialize history file if needed
if [ ! -f "$HISTORY_FILE" ]; then
echo "[]" > "$HISTORY_FILE"
fi
if command -v jq >/dev/null 2>&1; then
# Add new snapshot and trim to max size
local temp=$(mktemp)
jq --argjson snapshot "$snapshot" --argjson max "$MAX_HISTORY" \
'. + [$snapshot] | .[-$max:]' \
"$HISTORY_FILE" > "$temp" 2>/dev/null && mv "$temp" "$HISTORY_FILE"
else
# Simple append without jq (less efficient)
local current=$(cat "$HISTORY_FILE" 2>/dev/null | tr -d '[]')
if [ -n "$current" ]; then
echo "[$current,$snapshot]" > "$HISTORY_FILE"
else
echo "[$snapshot]" > "$HISTORY_FILE"
fi
fi
}
# Main collection routine
main() {
# Collect current stats
local snapshot=$(collect_stats)
if [ $? -eq 0 ] && [ -n "$snapshot" ]; then
# Save current snapshot
echo "$snapshot" > "$STATS_FILE"
# Add to history
add_to_history "$snapshot"
logger -t ndpid-collector "Stats collected: $(echo "$snapshot" | jsonfilter -e '@.flows_active' 2>/dev/null || echo 0) active flows"
fi
}
# Run modes
case "$1" in
--once|-o)
main
;;
--daemon|-d)
while true; do
main
sleep 60
done
;;
--status|-s)
cat "$STATS_FILE" 2>/dev/null || echo '{"error": "No stats available"}'
;;
--history|-h)
cat "$HISTORY_FILE" 2>/dev/null || echo '[]'
;;
*)
main
;;
esac

View File

@ -0,0 +1,210 @@
#!/bin/sh
# nDPId to Netifyd Compatibility Layer
# Translates nDPId events to Netifyd-compatible format
# Copyright (C) 2025 CyberMind.fr
. /lib/functions.sh
. /usr/share/ndpid/functions.sh 2>/dev/null || true
# Configuration
DISTRIBUTOR_SOCK="/var/run/ndpid/distributor.sock"
STATUS_FILE="/var/run/netifyd/status.json"
FLOWS_FILE="/tmp/ndpid-flows.json"
STATS_FILE="/tmp/ndpid-stats.json"
STATS_HISTORY="/tmp/ndpid-stats-history.json"
UPDATE_INTERVAL=1
MAX_HISTORY=1440
# State variables (stored in temp files for shell compatibility)
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"
# Initialize state
init_state() {
mkdir -p "$STATE_DIR"
mkdir -p "$(dirname "$STATUS_FILE")"
echo "0" > "$FLOWS_ACTIVE_FILE"
echo "0" > "$FLOW_COUNT_FILE"
echo "{}" > "$STATS_FILE_TMP"
}
# Increment counter
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)
[ "$val" -gt 0 ] && val=$((val - 1))
echo "$val" > "$file"
}
# Get counter value
get_counter() {
cat "$1" 2>/dev/null || echo 0
}
# Update interface stats
update_iface_stats() {
local iface="$1"
local proto="$2"
local bytes="$3"
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" '
.[$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
else . end
')
echo "$stats" > "$STATS_FILE_TMP"
fi
}
# Process a single nDPId event
process_event() {
local raw="$1"
# Strip 5-digit length prefix
local json="${raw:5}"
# 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)
case "$event_name" in
new)
inc_counter "$FLOW_COUNT_FILE"
inc_counter "$FLOWS_ACTIVE_FILE"
;;
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"
;;
esac
}
# Generate Netifyd-compatible status.json
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))
if command -v jq >/dev/null 2>&1; then
jq -n \
--argjson flow_count "$flow_count" \
--argjson flows_active "$flows_active" \
--argjson stats "$stats" \
--argjson uptime "$uptime" \
'{
flow_count: $flow_count,
flows_active: $flows_active,
stats: $stats,
devices: [],
dns_hint_cache: { cache_size: 0 },
uptime: $uptime,
source: "ndpid-compat"
}' > "$STATUS_FILE"
else
cat > "$STATUS_FILE" << EOF
{
"flow_count": $flow_count,
"flows_active": $flows_active,
"stats": $stats,
"devices": [],
"dns_hint_cache": { "cache_size": 0 },
"uptime": $uptime,
"source": "ndpid-compat"
}
EOF
fi
}
# Main loop
main() {
START_TIME=$(date +%s)
logger -t ndpid-compat "Starting nDPId compatibility layer"
# Initialize state
init_state
# Check for socat
if ! command -v socat >/dev/null 2>&1; then
logger -t ndpid-compat "ERROR: socat not found, using nc fallback"
USE_NC=1
fi
# Wait for distributor socket
local wait_count=0
while [ ! -S "$DISTRIBUTOR_SOCK" ] && [ $wait_count -lt 30 ]; do
sleep 1
wait_count=$((wait_count + 1))
done
if [ ! -S "$DISTRIBUTOR_SOCK" ]; then
logger -t ndpid-compat "ERROR: Distributor socket not found after 30s"
exit 1
fi
logger -t ndpid-compat "Connected to distributor: $DISTRIBUTOR_SOCK"
# Background status file updater
(
while true; do
generate_status
sleep $UPDATE_INTERVAL
done
) &
STATUS_PID=$!
trap "kill $STATUS_PID 2>/dev/null" EXIT
# Read events from distributor
if [ -z "$USE_NC" ]; then
socat -u UNIX-CONNECT:"$DISTRIBUTOR_SOCK" - | while IFS= read -r line; do
process_event "$line"
done
else
nc -U "$DISTRIBUTOR_SOCK" | while IFS= read -r line; do
process_event "$line"
done
fi
}
# Run main if not sourced
case "$1" in
-h|--help)
echo "Usage: $0 [-d|--daemon]"
echo " Translates nDPId events to Netifyd-compatible format"
exit 0
;;
-d|--daemon)
main &
echo $! > /var/run/ndpid-compat.pid
;;
*)
main
;;
esac

View File

@ -0,0 +1,161 @@
#!/bin/sh
# nDPId Flow Actions Handler
# Processes flow events and updates ipsets/nftables
# Copyright (C) 2025 CyberMind.fr
. /lib/functions.sh
. /usr/share/ndpid/functions.sh 2>/dev/null || true
DISTRIBUTOR_SOCK="/var/run/ndpid/distributor.sock"
CONFIG_FILE="/etc/config/ndpid"
# Load configuration
load_config() {
config_load ndpid
config_get_bool ACTIONS_ENABLED actions enabled 0
config_get BITTORRENT_IPSET actions bittorrent_ipset "secubox-bittorrent"
config_get BITTORRENT_TIMEOUT actions bittorrent_timeout 900
config_get STREAMING_IPSET actions streaming_ipset "secubox-streaming"
config_get STREAMING_TIMEOUT actions streaming_timeout 1800
config_get BLOCKED_IPSET actions blocked_ipset "secubox-blocked"
config_get BLOCKED_TIMEOUT actions blocked_timeout 3600
# Get blocked applications
BLOCKED_APPS=""
config_list_foreach actions blocked_app append_blocked_app
}
append_blocked_app() {
BLOCKED_APPS="$BLOCKED_APPS $1"
}
# Normalize application name from nDPI format
normalize_app() {
echo "$1" | tr '.' '\n' | tail -1 | tr '[:upper:]' '[:lower:]'
}
# Check if app is in blocked list
is_blocked_app() {
local app="$1"
for blocked in $BLOCKED_APPS; do
[ "$app" = "$blocked" ] && return 0
done
return 1
}
# Check if app is streaming service
is_streaming_app() {
local app="$1"
case "$app" in
netflix|youtube|spotify|twitch|disney|amazon_video|hulu|hbo|apple_tv|peacock)
return 0
;;
esac
return 1
}
# Process a flow detection event
process_detection() {
local json="$1"
# Extract fields
local src_ip=$(echo "$json" | jsonfilter -e '@.src_ip' 2>/dev/null)
local dst_ip=$(echo "$json" | jsonfilter -e '@.dst_ip' 2>/dev/null)
local ndpi_proto=$(echo "$json" | jsonfilter -e '@.ndpi.proto' 2>/dev/null)
[ -z "$ndpi_proto" ] && return
local app=$(normalize_app "$ndpi_proto")
# BitTorrent detection
if [ "$app" = "bittorrent" ]; then
logger -t ndpid-actions "BitTorrent detected: $src_ip -> $dst_ip"
ipset add "$BITTORRENT_IPSET" "$src_ip" timeout "$BITTORRENT_TIMEOUT" 2>/dev/null
ipset add "$BITTORRENT_IPSET" "$dst_ip" timeout "$BITTORRENT_TIMEOUT" 2>/dev/null
fi
# Streaming services detection
if is_streaming_app "$app"; then
ipset add "$STREAMING_IPSET" "$dst_ip" timeout "$STREAMING_TIMEOUT" 2>/dev/null
fi
# Blocked applications
if is_blocked_app "$app"; then
logger -t ndpid-actions "Blocked app detected: $app from $src_ip"
ipset add "$BLOCKED_IPSET" "$src_ip" timeout "$BLOCKED_TIMEOUT" 2>/dev/null
fi
}
# Main event processing loop
process_events() {
while IFS= read -r line; do
# Strip 5-digit length prefix
local json="${line:5}"
# Get event type
local event=$(echo "$json" | jsonfilter -e '@.flow_event_name' 2>/dev/null)
case "$event" in
detected|guessed)
process_detection "$json"
;;
esac
done
}
# Create ipsets if they don't exist
setup_ipsets() {
ipset list "$BITTORRENT_IPSET" >/dev/null 2>&1 || \
ipset create "$BITTORRENT_IPSET" hash:ip timeout "$BITTORRENT_TIMEOUT"
ipset list "$STREAMING_IPSET" >/dev/null 2>&1 || \
ipset create "$STREAMING_IPSET" hash:ip timeout "$STREAMING_TIMEOUT"
ipset list "$BLOCKED_IPSET" >/dev/null 2>&1 || \
ipset create "$BLOCKED_IPSET" hash:ip timeout "$BLOCKED_TIMEOUT"
}
# Main
main() {
load_config
if [ "$ACTIONS_ENABLED" -ne 1 ]; then
logger -t ndpid-actions "Flow actions disabled in config"
exit 0
fi
logger -t ndpid-actions "Starting flow actions handler"
# Setup ipsets
setup_ipsets
# Wait for socket
local wait_count=0
while [ ! -S "$DISTRIBUTOR_SOCK" ] && [ $wait_count -lt 30 ]; do
sleep 1
wait_count=$((wait_count + 1))
done
if [ ! -S "$DISTRIBUTOR_SOCK" ]; then
logger -t ndpid-actions "ERROR: Distributor socket not found"
exit 1
fi
# Connect and process events
if command -v socat >/dev/null 2>&1; then
socat -u UNIX-CONNECT:"$DISTRIBUTOR_SOCK" - | process_events
else
nc -U "$DISTRIBUTOR_SOCK" | process_events
fi
}
case "$1" in
-d|--daemon)
main &
echo $! > /var/run/ndpid-flow-actions.pid
;;
*)
main
;;
esac

View File

@ -0,0 +1,598 @@
#!/bin/sh
# SPDX-License-Identifier: MIT
# SecuBox nDPId - RPCD Backend
# Complete interface for nDPId DPI daemon
# Copyright (C) 2025 CyberMind.fr
. /lib/functions.sh
. /usr/share/libubox/jshn.sh
# Paths
CONFIG_FILE="/etc/config/ndpid"
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"
STATS_CACHE="/tmp/ndpid-stats.json"
LOG_FILE="/var/log/ndpid.log"
# Logging
log_msg() {
local level="$1"
shift
logger -t luci.ndpid "[$level] $*"
}
# Check if nDPId is running
check_ndpid_running() {
pidof ndpid >/dev/null 2>&1
}
# Check if nDPIsrvd is running
check_ndpisrvd_running() {
pidof ndpisrvd >/dev/null 2>&1
}
# Get service status
get_service_status() {
json_init
local running=0
local distributor_running=0
local pid=""
local uptime=0
local version=""
if check_ndpid_running; then
running=1
pid=$(pidof ndpid | awk '{print $1}')
# Get uptime from /proc
if [ -n "$pid" ] && [ -d "/proc/$pid" ]; then
local start_time=$(stat -c %Y /proc/$pid 2>/dev/null)
local now=$(date +%s)
[ -n "$start_time" ] && uptime=$((now - start_time))
fi
# Get version
version=$(ndpid -v 2>&1 | head -1 | grep -oE '[0-9]+\.[0-9]+' || echo "1.7")
fi
check_ndpisrvd_running && distributor_running=1
json_add_boolean "running" "$running"
json_add_boolean "distributor_running" "$distributor_running"
json_add_string "pid" "$pid"
json_add_int "uptime" "$uptime"
json_add_string "version" "$version"
json_add_string "collector_socket" "$COLLECTOR_SOCK"
json_add_string "distributor_socket" "$DISTRIBUTOR_SOCK"
# Check if compat layer is running
local compat_running=0
pidof ndpid-compat >/dev/null 2>&1 && compat_running=1
json_add_boolean "compat_running" "$compat_running"
json_dump
}
# Get real-time flow statistics from status file
get_realtime_flows() {
json_init
if [ -f "$STATUS_FILE" ]; then
local status_json=$(cat "$STATUS_FILE" 2>/dev/null)
if [ -n "$status_json" ]; then
# Parse values
local flow_count=$(echo "$status_json" | jsonfilter -e '@.flow_count' 2>/dev/null || echo 0)
local flows_active=$(echo "$status_json" | jsonfilter -e '@.flows_active' 2>/dev/null || echo 0)
local uptime=$(echo "$status_json" | jsonfilter -e '@.uptime' 2>/dev/null || echo 0)
json_add_int "flow_count" "$flow_count"
json_add_int "flows_active" "$flows_active"
json_add_int "uptime" "$uptime"
json_add_boolean "available" 1
else
json_add_boolean "available" 0
fi
else
json_add_boolean "available" 0
json_add_int "flow_count" 0
json_add_int "flows_active" 0
fi
json_dump
}
# Get interface statistics
get_interface_stats() {
json_init
json_add_array "interfaces"
if [ -f "$STATUS_FILE" ]; then
local stats=$(cat "$STATUS_FILE" | jsonfilter -e '@.stats' 2>/dev/null)
if [ -n "$stats" ] && command -v jq >/dev/null 2>&1; then
# Parse each interface
for iface in $(echo "$stats" | jq -r 'keys[]' 2>/dev/null); do
json_add_object
json_add_string "name" "$iface"
local ip_bytes=$(echo "$stats" | jq -r ".\"$iface\".ip_bytes // 0")
local wire_bytes=$(echo "$stats" | jq -r ".\"$iface\".wire_bytes // 0")
local tcp=$(echo "$stats" | jq -r ".\"$iface\".tcp // 0")
local udp=$(echo "$stats" | jq -r ".\"$iface\".udp // 0")
local icmp=$(echo "$stats" | jq -r ".\"$iface\".icmp // 0")
json_add_int "ip_bytes" "$ip_bytes"
json_add_int "wire_bytes" "$wire_bytes"
json_add_int "tcp" "$tcp"
json_add_int "udp" "$udp"
json_add_int "icmp" "$icmp"
json_close_object
done
fi
fi
json_close_array
json_dump
}
# Get top applications (from flow analysis)
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) |
map({
name: .[0].application,
flows: length,
bytes: (map(.bytes_rx + .bytes_tx) | add)
}) |
sort_by(-.bytes) |
.[0:10][] |
"\(.name)|\(.flows)|\(.bytes)"
' "$FLOWS_CACHE" 2>/dev/null | while IFS='|' read -r name flows bytes; do
json_add_object
json_add_string "name" "${name:-unknown}"
json_add_int "flows" "${flows:-0}"
json_add_int "bytes" "${bytes:-0}"
json_close_object
done
fi
json_close_array
json_dump
}
# Get top protocols
get_top_protocols() {
json_init
json_add_array "protocols"
if [ -f "$STATUS_FILE" ]; then
local stats=$(cat "$STATUS_FILE" | jsonfilter -e '@.stats' 2>/dev/null)
if [ -n "$stats" ] && command -v jq >/dev/null 2>&1; then
# Aggregate protocol counts across interfaces
local tcp=$(echo "$stats" | jq '[.[].tcp] | add // 0')
local udp=$(echo "$stats" | jq '[.[].udp] | add // 0')
local icmp=$(echo "$stats" | jq '[.[].icmp] | add // 0')
json_add_object
json_add_string "name" "TCP"
json_add_int "count" "$tcp"
json_close_object
json_add_object
json_add_string "name" "UDP"
json_add_int "count" "$udp"
json_close_object
json_add_object
json_add_string "name" "ICMP"
json_add_int "count" "$icmp"
json_close_object
fi
fi
json_close_array
json_dump
}
# Get configuration
get_config() {
json_init
config_load ndpid
# Main settings
local enabled interfaces collector_socket max_flows
config_get_bool enabled main enabled 0
config_get interfaces main interface ""
config_get collector_socket main collector_socket "/var/run/ndpid/collector.sock"
config_get max_flows main max_flows 100000
json_add_boolean "enabled" "$enabled"
json_add_string "interfaces" "$interfaces"
json_add_string "collector_socket" "$collector_socket"
json_add_int "max_flows" "$max_flows"
# Distributor settings
local dist_enabled tcp_port tcp_address
config_get_bool dist_enabled distributor enabled 1
config_get tcp_port distributor tcp_port 7000
config_get tcp_address distributor tcp_address "127.0.0.1"
json_add_object "distributor"
json_add_boolean "enabled" "$dist_enabled"
json_add_int "tcp_port" "$tcp_port"
json_add_string "tcp_address" "$tcp_address"
json_close_object
# Compat settings
local compat_enabled
config_get_bool compat_enabled compat enabled 1
json_add_object "compat"
json_add_boolean "enabled" "$compat_enabled"
json_close_object
# Actions settings
local actions_enabled
config_get_bool actions_enabled actions enabled 0
json_add_object "actions"
json_add_boolean "enabled" "$actions_enabled"
json_close_object
json_dump
}
# Get dashboard summary
get_dashboard() {
json_init
# Service status
json_add_object "service"
local running=0
check_ndpid_running && running=1
json_add_boolean "running" "$running"
local distributor_running=0
check_ndpisrvd_running && distributor_running=1
json_add_boolean "distributor_running" "$distributor_running"
local compat_running=0
pidof ndpid-compat >/dev/null 2>&1 && compat_running=1
json_add_boolean "compat_running" "$compat_running"
json_add_string "version" "$(ndpid -v 2>&1 | head -1 | grep -oE '[0-9]+\.[0-9]+' || echo '1.7')"
json_close_object
# Flow stats
json_add_object "flows"
if [ -f "$STATUS_FILE" ]; then
local flow_count=$(cat "$STATUS_FILE" | jsonfilter -e '@.flow_count' 2>/dev/null || echo 0)
local flows_active=$(cat "$STATUS_FILE" | jsonfilter -e '@.flows_active' 2>/dev/null || echo 0)
json_add_int "total" "$flow_count"
json_add_int "active" "$flows_active"
else
json_add_int "total" 0
json_add_int "active" 0
fi
json_close_object
# System stats
json_add_object "system"
local pid=$(pidof ndpid | awk '{print $1}')
if [ -n "$pid" ] && [ -d "/proc/$pid" ]; then
# Memory usage
local mem_kb=$(awk '/VmRSS/{print $2}' /proc/$pid/status 2>/dev/null || echo 0)
json_add_int "memory_kb" "$mem_kb"
# CPU (simplified)
json_add_string "cpu" "0%"
else
json_add_int "memory_kb" 0
json_add_string "cpu" "0%"
fi
json_close_object
# Interfaces
json_add_array "interfaces"
config_load ndpid
config_get interfaces main interface ""
for iface in $interfaces; do
json_add_string "" "$iface"
done
json_close_array
json_dump
}
# Service control: start
service_start() {
json_init
/etc/init.d/ndpisrvd start 2>&1
sleep 1
/etc/init.d/ndpid start 2>&1
# Start compat layer
local compat_enabled
config_load ndpid
config_get_bool compat_enabled compat enabled 1
if [ "$compat_enabled" -eq 1 ]; then
/usr/bin/ndpid-compat -d 2>&1
fi
sleep 2
if check_ndpid_running; then
json_add_boolean "success" 1
json_add_string "message" "nDPId started successfully"
log_msg "INFO" "Service started"
else
json_add_boolean "success" 0
json_add_string "message" "Failed to start nDPId"
log_msg "ERROR" "Service failed to start"
fi
json_dump
}
# Service control: stop
service_stop() {
json_init
# Stop compat layer
if [ -f /var/run/ndpid-compat.pid ]; then
kill $(cat /var/run/ndpid-compat.pid) 2>/dev/null
rm -f /var/run/ndpid-compat.pid
fi
/etc/init.d/ndpid stop 2>&1
/etc/init.d/ndpisrvd stop 2>&1
sleep 1
if ! check_ndpid_running; then
json_add_boolean "success" 1
json_add_string "message" "nDPId stopped successfully"
log_msg "INFO" "Service stopped"
else
json_add_boolean "success" 0
json_add_string "message" "Failed to stop nDPId"
log_msg "ERROR" "Service failed to stop"
fi
json_dump
}
# Service control: restart
service_restart() {
json_init
service_stop >/dev/null 2>&1
sleep 2
service_start >/dev/null 2>&1
if check_ndpid_running; then
json_add_boolean "success" 1
json_add_string "message" "nDPId restarted successfully"
else
json_add_boolean "success" 0
json_add_string "message" "Failed to restart nDPId"
fi
json_dump
}
# Service control: enable
service_enable() {
json_init
uci set ndpid.main.enabled=1
uci commit ndpid
/etc/init.d/ndpid enable
/etc/init.d/ndpisrvd enable
json_add_boolean "success" 1
json_add_string "message" "nDPId enabled"
json_dump
}
# Service control: disable
service_disable() {
json_init
uci set ndpid.main.enabled=0
uci commit ndpid
/etc/init.d/ndpid disable
/etc/init.d/ndpisrvd disable
json_add_boolean "success" 1
json_add_string "message" "nDPId disabled"
json_dump
}
# Update configuration
update_config() {
local data="$1"
json_init
if [ -z "$data" ]; then
json_add_boolean "success" 0
json_add_string "error" "No configuration data provided"
json_dump
return
fi
# Parse and apply configuration
local enabled=$(echo "$data" | jsonfilter -e '@.enabled' 2>/dev/null)
local interfaces=$(echo "$data" | jsonfilter -e '@.interfaces' 2>/dev/null)
local max_flows=$(echo "$data" | jsonfilter -e '@.max_flows' 2>/dev/null)
[ -n "$enabled" ] && uci set ndpid.main.enabled="$enabled"
[ -n "$max_flows" ] && uci set ndpid.main.max_flows="$max_flows"
# Handle interfaces (clear and re-add)
if [ -n "$interfaces" ]; then
uci -q delete ndpid.main.interface
for iface in $interfaces; do
uci add_list ndpid.main.interface="$iface"
done
fi
uci commit ndpid
json_add_boolean "success" 1
json_add_string "message" "Configuration updated"
log_msg "INFO" "Configuration updated"
json_dump
}
# Clear statistics cache
clear_cache() {
json_init
rm -f "$FLOWS_CACHE" "$STATS_CACHE"
rm -rf /tmp/ndpid-state
json_add_boolean "success" 1
json_add_string "message" "Cache cleared"
json_dump
}
# Get monitored interfaces list
get_interfaces() {
json_init
json_add_array "interfaces"
# Get configured interfaces
config_load ndpid
config_get interfaces main interface ""
for iface in $interfaces; do
json_add_object
json_add_string "name" "$iface"
# Check if interface exists
if [ -d "/sys/class/net/$iface" ]; then
json_add_boolean "exists" 1
# Get interface state
local state=$(cat /sys/class/net/$iface/operstate 2>/dev/null || echo "unknown")
json_add_string "state" "$state"
# Get MAC address
local mac=$(cat /sys/class/net/$iface/address 2>/dev/null || echo "")
json_add_string "mac" "$mac"
else
json_add_boolean "exists" 0
fi
json_close_object
done
json_close_array
# Also list available interfaces
json_add_array "available"
for iface in $(ls /sys/class/net/ 2>/dev/null | grep -E '^(br-|eth|wlan)'); do
json_add_string "" "$iface"
done
json_close_array
json_dump
}
# RPC method dispatcher
case "$1" in
list)
cat << 'EOF'
{
"get_service_status": {},
"get_realtime_flows": {},
"get_interface_stats": {},
"get_top_applications": {},
"get_top_protocols": {},
"get_config": {},
"get_dashboard": {},
"get_interfaces": {},
"service_start": {},
"service_stop": {},
"service_restart": {},
"service_enable": {},
"service_disable": {},
"update_config": { "data": "object" },
"clear_cache": {}
}
EOF
;;
call)
case "$2" in
get_service_status)
get_service_status
;;
get_realtime_flows)
get_realtime_flows
;;
get_interface_stats)
get_interface_stats
;;
get_top_applications)
get_top_applications
;;
get_top_protocols)
get_top_protocols
;;
get_config)
get_config
;;
get_dashboard)
get_dashboard
;;
get_interfaces)
get_interfaces
;;
service_start)
service_start
;;
service_stop)
service_stop
;;
service_restart)
service_restart
;;
service_enable)
service_enable
;;
service_disable)
service_disable
;;
update_config)
read -r input
data=$(echo "$input" | jsonfilter -e '@.data' 2>/dev/null)
update_config "$data"
;;
clear_cache)
clear_cache
;;
*)
echo '{"error": "Unknown method"}'
;;
esac
;;
*)
echo '{"error": "Invalid action"}'
;;
esac

View File

@ -0,0 +1,37 @@
{
"admin/services/ndpid": {
"title": "nDPId",
"order": 60,
"action": {
"type": "firstchild"
},
"depends": {
"acl": ["luci-app-ndpid"],
"uci": {"ndpid": true}
}
},
"admin/services/ndpid/dashboard": {
"title": "Dashboard",
"order": 10,
"action": {
"type": "view",
"path": "ndpid/dashboard"
}
},
"admin/services/ndpid/flows": {
"title": "Flows",
"order": 20,
"action": {
"type": "view",
"path": "ndpid/flows"
}
},
"admin/services/ndpid/settings": {
"title": "Settings",
"order": 30,
"action": {
"type": "view",
"path": "ndpid/settings"
}
}
}

View File

@ -0,0 +1,34 @@
{
"luci-app-ndpid": {
"description": "Grant access to nDPId DPI dashboard",
"read": {
"ubus": {
"luci.ndpid": [
"get_service_status",
"get_realtime_flows",
"get_interface_stats",
"get_top_applications",
"get_top_protocols",
"get_config",
"get_dashboard",
"get_interfaces"
]
},
"uci": ["ndpid"]
},
"write": {
"ubus": {
"luci.ndpid": [
"service_start",
"service_stop",
"service_restart",
"service_enable",
"service_disable",
"update_config",
"clear_cache"
]
},
"uci": ["ndpid"]
}
}
}

View File

@ -0,0 +1,122 @@
#
# Copyright (C) 2025 CyberMind.fr (SecuBox Integration)
#
# This is free software, licensed under the GNU General Public License v3.
#
# nDPId - Lightweight Deep Packet Inspection Daemon
# Builds nDPId with bundled libndpi (requires >= 5.0, OpenWrt has 4.8)
#
include $(TOPDIR)/rules.mk
PKG_NAME:=ndpid
PKG_VERSION:=1.7.1
PKG_RELEASE:=1
# Use git dev branch for latest libndpi compatibility
# Version 1.7 (Oct 2023) has API incompatibilities with current libndpi
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/utoni/nDPId.git
PKG_SOURCE_VERSION:=f712dbacfbe80f5a3a30e784f59616a2dc63727f
PKG_MIRROR_HASH:=skip
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
PKG_LICENSE:=GPL-3.0-or-later
PKG_LICENSE_FILES:=COPYING
PKG_BUILD_PARALLEL:=1
# Out-of-source build required by nDPId
CMAKE_BINARY_SUBDIR:=build
include $(INCLUDE_DIR)/package.mk
include $(INCLUDE_DIR)/cmake.mk
# Fix: CMake passes ninja as MAKE_PROGRAM but libndpi uses autotools (make)
define Build/Prepare
$(call Build/Prepare/Default)
$(SED) 's|MAKE_PROGRAM=.*CMAKE_MAKE_PROGRAM.*|MAKE_PROGRAM=make|' \
$(PKG_BUILD_DIR)/CMakeLists.txt
endef
define Package/ndpid
SECTION:=net
CATEGORY:=Network
TITLE:=nDPId - Lightweight Deep Packet Inspection Daemon
URL:=https://github.com/utoni/nDPId
DEPENDS:=+libpcap +libjson-c +libpthread +zlib +libstdcpp
endef
define Package/ndpid/description
nDPId is a set of daemons and tools to capture, process and classify
network traffic using nDPI. It provides a lightweight alternative to
netifyd with a microservice architecture.
Components:
- nDPId: Traffic capture and DPI daemon
- nDPIsrvd: Event distribution broker
Note: Builds with bundled libndpi 5.x (OpenWrt feeds have 4.8 which is too old)
endef
define Package/ndpid/conffiles
/etc/config/ndpid
/etc/ndpid.conf
endef
# Build with bundled nDPI (fetches and builds libndpi automatically)
CMAKE_OPTIONS += \
-DBUILD_NDPI=ON \
-DENABLE_SYSTEMD=OFF \
-DENABLE_ZLIB=ON \
-DBUILD_EXAMPLES=OFF \
-DBUILD_DAEMON=ON \
-DENABLE_MEMORY_PROFILING=OFF \
-DENABLE_SANITIZER=OFF \
-DENABLE_COVERAGE=OFF
define Package/ndpid/install
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/build/nDPId $(1)/usr/sbin/ndpid
$(INSTALL_BIN) $(PKG_BUILD_DIR)/build/nDPIsrvd $(1)/usr/sbin/ndpisrvd
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/build/nDPId-test $(1)/usr/bin/ndpid-test 2>/dev/null || true
$(INSTALL_DIR) $(1)/etc
$(INSTALL_CONF) ./files/ndpid.conf $(1)/etc/ndpid.conf
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) ./files/ndpid.config $(1)/etc/config/ndpid
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) ./files/ndpid.init $(1)/etc/init.d/ndpid
$(INSTALL_BIN) ./files/ndpisrvd.init $(1)/etc/init.d/ndpisrvd
$(INSTALL_DIR) $(1)/usr/share/ndpid
$(INSTALL_DATA) ./files/functions.sh $(1)/usr/share/ndpid/
endef
define Package/ndpid/postinst
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || {
mkdir -p /var/run/ndpid
/etc/init.d/ndpisrvd enable
/etc/init.d/ndpid enable
echo "nDPId installed. Start with: /etc/init.d/ndpid start"
}
exit 0
endef
define Package/ndpid/prerm
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || {
/etc/init.d/ndpid stop
/etc/init.d/ndpisrvd stop
/etc/init.d/ndpid disable
/etc/init.d/ndpisrvd disable
}
exit 0
endef
$(eval $(call BuildPackage,ndpid))

View File

@ -0,0 +1,99 @@
#!/bin/sh
# nDPId shared functions for SecuBox
# Copyright (C) 2025 CyberMind.fr
# Paths
NDPID_RUNTIME_DIR="/var/run/ndpid"
NDPID_COLLECTOR_SOCK="${NDPID_RUNTIME_DIR}/collector.sock"
NDPID_DISTRIBUTOR_SOCK="${NDPID_RUNTIME_DIR}/distributor.sock"
NDPID_COMPAT_STATUS="/var/run/netifyd/status.json"
NDPID_FLOWS_FILE="/tmp/ndpid-flows.json"
NDPID_STATS_FILE="/tmp/ndpid-stats.json"
# Check if nDPId is running
ndpid_running() {
pidof ndpid >/dev/null 2>&1
}
# Check if nDPIsrvd is running
ndpisrvd_running() {
pidof ndpisrvd >/dev/null 2>&1
}
# Get nDPId version
ndpid_version() {
ndpid -v 2>&1 | head -1 | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?'
}
# Format bytes to human readable
format_bytes() {
local bytes="${1:-0}"
if [ "$bytes" -ge 1073741824 ]; then
echo "$(awk "BEGIN {printf \"%.2f\", $bytes/1073741824}") GB"
elif [ "$bytes" -ge 1048576 ]; then
echo "$(awk "BEGIN {printf \"%.2f\", $bytes/1048576}") MB"
elif [ "$bytes" -ge 1024 ]; then
echo "$(awk "BEGIN {printf \"%.2f\", $bytes/1024}") KB"
else
echo "${bytes} B"
fi
}
# Parse nDPId JSON event (strip 5-digit length prefix)
parse_ndpid_event() {
local raw="$1"
echo "${raw:5}"
}
# Extract application name from nDPI proto string
# e.g., "TLS.Google" -> "google", "QUIC.YouTube" -> "youtube"
normalize_app_name() {
local proto="$1"
echo "$proto" | tr '.' '\n' | tail -1 | tr '[:upper:]' '[:lower:]'
}
# Get list of network interfaces suitable for monitoring
get_monitor_interfaces() {
local ifaces=""
# Get bridge interfaces
for br in $(ls /sys/class/net/ 2>/dev/null | grep -E '^br-'); do
ifaces="$ifaces $br"
done
# Get ethernet interfaces if no bridges
if [ -z "$ifaces" ]; then
for eth in $(ls /sys/class/net/ 2>/dev/null | grep -E '^eth[0-9]'); do
ifaces="$ifaces $eth"
done
fi
echo "$ifaces" | xargs
}
# Create ipsets for flow actions
create_action_ipsets() {
# BitTorrent tracking
ipset list secubox-bittorrent >/dev/null 2>&1 || \
ipset create secubox-bittorrent hash:ip timeout 900 2>/dev/null
# Streaming services tracking
ipset list secubox-streaming >/dev/null 2>&1 || \
ipset create secubox-streaming hash:ip timeout 1800 2>/dev/null
# Blocked IPs
ipset list secubox-blocked >/dev/null 2>&1 || \
ipset create secubox-blocked hash:ip timeout 3600 2>/dev/null
}
# Add IP to ipset with timeout
add_to_ipset() {
local ipset_name="$1"
local ip="$2"
local timeout="${3:-900}"
ipset add "$ipset_name" "$ip" timeout "$timeout" 2>/dev/null
}
# Log message
ndpid_log() {
local level="${1:-INFO}"
shift
logger -t ndpid "[$level] $*"
}

View File

@ -0,0 +1,28 @@
# nDPId native configuration
# This file is auto-generated from UCI config
# Manual changes will be overwritten
# Collector socket for nDPIsrvd
collector = /var/run/ndpid/collector.sock
# Daemon settings
user = nobody
group = nogroup
# Flow settings
max-flows = 100000
flow-scan-interval = 10000
# Timeouts (milliseconds)
generic-max-idle-time = 600000
icmp-max-idle-time = 120000
udp-max-idle-time = 180000
tcp-max-idle-time = 7200000
tcp-max-post-end-flow-time = 120000
# Compression
enable-zlib-compression = yes
# Error thresholds
max-packets-per-flow-to-send = 32
max-packets-per-flow-to-process = 32

View File

@ -0,0 +1,58 @@
# nDPId Configuration for SecuBox
# /etc/config/ndpid
config ndpid 'main'
option enabled '0'
option user 'nobody'
option group 'nogroup'
# Interfaces to monitor (space-separated)
list interface 'br-lan'
# Collector socket path
option collector_socket '/var/run/ndpid/collector.sock'
# Enable packet capture
option pcap_filter ''
# Max flows to track
option max_flows '100000'
# Flow idle timeout (ms)
option flow_idle_timeout '600000'
# TCP flow timeout (ms)
option tcp_timeout '7200000'
# UDP flow timeout (ms)
option udp_timeout '180000'
# Enable compression
option compression '1'
config ndpisrvd 'distributor'
option enabled '1'
# Listen socket for consumers
option listen_socket '/var/run/ndpid/distributor.sock'
# TCP listen port (0 = disabled)
option tcp_port '7000'
option tcp_address '127.0.0.1'
# Max clients
option max_clients '10'
config compat 'compat'
# Enable Netifyd compatibility layer
option enabled '1'
# Output paths (Netifyd-compatible)
option status_file '/var/run/netifyd/status.json'
option flows_file '/tmp/ndpid-flows.json'
# Update interval (seconds)
option update_interval '1'
config actions 'actions'
# Enable flow actions (ipset/nftables)
option enabled '0'
# BitTorrent detection
option bittorrent_ipset 'secubox-bittorrent'
option bittorrent_timeout '900'
# Streaming services
option streaming_ipset 'secubox-streaming'
option streaming_timeout '1800'
# Blocked categories
option blocked_ipset 'secubox-blocked'
option blocked_timeout '3600'
# List of blocked applications
list blocked_app 'bittorrent'
list blocked_app 'tor'

View File

@ -0,0 +1,116 @@
#!/bin/sh /etc/rc.common
# nDPId init script for OpenWrt
# Copyright (C) 2025 CyberMind.fr
START=51
STOP=10
USE_PROCD=1
PROG=/usr/sbin/ndpid
CONF=/etc/config/ndpid
RUNTIME_DIR=/var/run/ndpid
COMPAT_STATUS=/var/run/netifyd/status.json
. /usr/share/ndpid/functions.sh 2>/dev/null || true
validate_section() {
uci_load_validate ndpid main "$1" "$2" \
'enabled:bool:0' \
'user:string:nobody' \
'group:string:nogroup' \
'interface:list(string)' \
'collector_socket:string:/var/run/ndpid/collector.sock' \
'pcap_filter:string' \
'max_flows:uinteger:100000' \
'flow_idle_timeout:uinteger:600000' \
'tcp_timeout:uinteger:7200000' \
'udp_timeout:uinteger:180000' \
'compression:bool:1'
}
generate_config() {
local enabled user group collector_socket max_flows
local flow_idle_timeout tcp_timeout udp_timeout compression
config_load ndpid
config_get enabled main enabled 0
config_get user main user nobody
config_get group main group nogroup
config_get collector_socket main collector_socket /var/run/ndpid/collector.sock
config_get max_flows main max_flows 100000
config_get flow_idle_timeout main flow_idle_timeout 600000
config_get tcp_timeout main tcp_timeout 7200000
config_get udp_timeout main udp_timeout 180000
config_get_bool compression main compression 1
cat > /etc/ndpid.conf << EOF
# Auto-generated from UCI - do not edit
collector = $collector_socket
user = $user
group = $group
max-flows = $max_flows
generic-max-idle-time = $flow_idle_timeout
tcp-max-idle-time = $tcp_timeout
udp-max-idle-time = $udp_timeout
EOF
[ "$compression" -eq 1 ] && echo "enable-zlib-compression = yes" >> /etc/ndpid.conf
}
start_service() {
local enabled interfaces
config_load ndpid
config_get_bool enabled main enabled 0
[ "$enabled" -eq 0 ] && {
logger -t ndpid "Service disabled in config"
return 0
}
# Create runtime directories
mkdir -p "$RUNTIME_DIR"
mkdir -p "$(dirname "$COMPAT_STATUS")"
chown nobody:nogroup "$RUNTIME_DIR"
# Generate native config from UCI
generate_config
# Get interfaces
config_get interfaces main interface "br-lan"
# Build interface arguments
local iface_args=""
for iface in $interfaces; do
iface_args="$iface_args -i $iface"
done
# Get collector socket
local collector_socket
config_get collector_socket main collector_socket /var/run/ndpid/collector.sock
logger -t ndpid "Starting nDPId on interfaces: $interfaces"
procd_open_instance ndpid
procd_set_param command "$PROG" \
-c "$collector_socket" \
$iface_args
procd_set_param respawn ${respawn_threshold:-3600} ${respawn_timeout:-5} ${respawn_retry:-5}
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param pidfile /var/run/ndpid.pid
procd_close_instance
}
stop_service() {
logger -t ndpid "Stopping nDPId"
}
reload_service() {
stop
start
}
service_triggers() {
procd_add_reload_trigger "ndpid"
}

View File

@ -0,0 +1,68 @@
#!/bin/sh /etc/rc.common
# nDPIsrvd init script for OpenWrt
# Event distribution broker for nDPId
# Copyright (C) 2025 CyberMind.fr
START=50
STOP=11
USE_PROCD=1
PROG=/usr/sbin/ndpisrvd
CONF=/etc/config/ndpid
RUNTIME_DIR=/var/run/ndpid
start_service() {
local enabled listen_socket tcp_port tcp_address max_clients
local collector_socket
config_load ndpid
# Check distributor settings
config_get_bool enabled distributor enabled 1
[ "$enabled" -eq 0 ] && {
logger -t ndpisrvd "Service disabled in config"
return 0
}
# Create runtime directory
mkdir -p "$RUNTIME_DIR"
chown nobody:nogroup "$RUNTIME_DIR"
# Get configuration
config_get collector_socket main collector_socket /var/run/ndpid/collector.sock
config_get listen_socket distributor listen_socket /var/run/ndpid/distributor.sock
config_get tcp_port distributor tcp_port 7000
config_get tcp_address distributor tcp_address 127.0.0.1
config_get max_clients distributor max_clients 10
logger -t ndpisrvd "Starting nDPIsrvd (collector: $collector_socket, distributor: $listen_socket)"
# Build command
local cmd_args="-c $collector_socket -s $listen_socket"
# Add TCP listener if enabled
[ "$tcp_port" -gt 0 ] && {
cmd_args="$cmd_args -S ${tcp_address}:${tcp_port}"
}
procd_open_instance ndpisrvd
procd_set_param command "$PROG" $cmd_args
procd_set_param respawn ${respawn_threshold:-3600} ${respawn_timeout:-5} ${respawn_retry:-5}
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param pidfile /var/run/ndpisrvd.pid
procd_close_instance
}
stop_service() {
logger -t ndpisrvd "Stopping nDPIsrvd"
}
reload_service() {
stop
start
}
service_triggers() {
procd_add_reload_trigger "ndpid"
}