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