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:
parent
25385fc35d
commit
e4a553a6d5
682
DOCS/MIGRATION-NETIFYD-TO-NDPID.md
Normal file
682
DOCS/MIGRATION-NETIFYD-TO-NDPID.md
Normal 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*
|
||||
35
package/secubox/luci-app-ndpid/Makefile
Normal file
35
package/secubox/luci-app-ndpid/Makefile
Normal 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
|
||||
@ -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';
|
||||
}
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
@ -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
|
||||
});
|
||||
@ -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
|
||||
});
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
42
package/secubox/luci-app-ndpid/root/etc/init.d/ndpid-compat
Normal file
42
package/secubox/luci-app-ndpid/root/etc/init.d/ndpid-compat
Normal 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"
|
||||
}
|
||||
112
package/secubox/luci-app-ndpid/root/usr/bin/ndpid-collector
Normal file
112
package/secubox/luci-app-ndpid/root/usr/bin/ndpid-collector
Normal 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
|
||||
210
package/secubox/luci-app-ndpid/root/usr/bin/ndpid-compat
Normal file
210
package/secubox/luci-app-ndpid/root/usr/bin/ndpid-compat
Normal 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
|
||||
161
package/secubox/luci-app-ndpid/root/usr/bin/ndpid-flow-actions
Normal file
161
package/secubox/luci-app-ndpid/root/usr/bin/ndpid-flow-actions
Normal 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
|
||||
598
package/secubox/luci-app-ndpid/root/usr/libexec/rpcd/luci.ndpid
Normal file
598
package/secubox/luci-app-ndpid/root/usr/libexec/rpcd/luci.ndpid
Normal 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
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
122
package/secubox/secubox-app-ndpid/Makefile
Normal file
122
package/secubox/secubox-app-ndpid/Makefile
Normal 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))
|
||||
99
package/secubox/secubox-app-ndpid/files/functions.sh
Normal file
99
package/secubox/secubox-app-ndpid/files/functions.sh
Normal 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] $*"
|
||||
}
|
||||
28
package/secubox/secubox-app-ndpid/files/ndpid.conf
Normal file
28
package/secubox/secubox-app-ndpid/files/ndpid.conf
Normal 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
|
||||
58
package/secubox/secubox-app-ndpid/files/ndpid.config
Normal file
58
package/secubox/secubox-app-ndpid/files/ndpid.config
Normal 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'
|
||||
116
package/secubox/secubox-app-ndpid/files/ndpid.init
Normal file
116
package/secubox/secubox-app-ndpid/files/ndpid.init
Normal 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"
|
||||
}
|
||||
68
package/secubox/secubox-app-ndpid/files/ndpisrvd.init
Normal file
68
package/secubox/secubox-app-ndpid/files/ndpisrvd.init
Normal 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"
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user