From e4a553a6d5dda46c3551363526f7dbffdaa0714f Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Fri, 9 Jan 2026 09:32:23 +0100 Subject: [PATCH] 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 --- DOCS/MIGRATION-NETIFYD-TO-NDPID.md | 682 ++++++++++++++++++ package/secubox/luci-app-ndpid/Makefile | 35 + .../htdocs/luci-static/resources/ndpid/api.js | 207 ++++++ .../luci-static/resources/ndpid/dashboard.css | 529 ++++++++++++++ .../resources/view/ndpid/dashboard.js | 364 ++++++++++ .../luci-static/resources/view/ndpid/flows.js | 239 ++++++ .../resources/view/ndpid/settings.js | 165 +++++ .../root/etc/init.d/ndpid-compat | 42 ++ .../root/usr/bin/ndpid-collector | 112 +++ .../luci-app-ndpid/root/usr/bin/ndpid-compat | 210 ++++++ .../root/usr/bin/ndpid-flow-actions | 161 +++++ .../root/usr/libexec/rpcd/luci.ndpid | 598 +++++++++++++++ .../usr/share/luci/menu.d/luci-app-ndpid.json | 37 + .../usr/share/rpcd/acl.d/luci-app-ndpid.json | 34 + package/secubox/secubox-app-ndpid/Makefile | 122 ++++ .../secubox-app-ndpid/files/functions.sh | 99 +++ .../secubox-app-ndpid/files/ndpid.conf | 28 + .../secubox-app-ndpid/files/ndpid.config | 58 ++ .../secubox-app-ndpid/files/ndpid.init | 116 +++ .../secubox-app-ndpid/files/ndpisrvd.init | 68 ++ 20 files changed, 3906 insertions(+) create mode 100644 DOCS/MIGRATION-NETIFYD-TO-NDPID.md create mode 100644 package/secubox/luci-app-ndpid/Makefile create mode 100644 package/secubox/luci-app-ndpid/htdocs/luci-static/resources/ndpid/api.js create mode 100644 package/secubox/luci-app-ndpid/htdocs/luci-static/resources/ndpid/dashboard.css create mode 100644 package/secubox/luci-app-ndpid/htdocs/luci-static/resources/view/ndpid/dashboard.js create mode 100644 package/secubox/luci-app-ndpid/htdocs/luci-static/resources/view/ndpid/flows.js create mode 100644 package/secubox/luci-app-ndpid/htdocs/luci-static/resources/view/ndpid/settings.js create mode 100644 package/secubox/luci-app-ndpid/root/etc/init.d/ndpid-compat create mode 100644 package/secubox/luci-app-ndpid/root/usr/bin/ndpid-collector create mode 100644 package/secubox/luci-app-ndpid/root/usr/bin/ndpid-compat create mode 100644 package/secubox/luci-app-ndpid/root/usr/bin/ndpid-flow-actions create mode 100644 package/secubox/luci-app-ndpid/root/usr/libexec/rpcd/luci.ndpid create mode 100644 package/secubox/luci-app-ndpid/root/usr/share/luci/menu.d/luci-app-ndpid.json create mode 100644 package/secubox/luci-app-ndpid/root/usr/share/rpcd/acl.d/luci-app-ndpid.json create mode 100644 package/secubox/secubox-app-ndpid/Makefile create mode 100644 package/secubox/secubox-app-ndpid/files/functions.sh create mode 100644 package/secubox/secubox-app-ndpid/files/ndpid.conf create mode 100644 package/secubox/secubox-app-ndpid/files/ndpid.config create mode 100644 package/secubox/secubox-app-ndpid/files/ndpid.init create mode 100644 package/secubox/secubox-app-ndpid/files/ndpisrvd.init diff --git a/DOCS/MIGRATION-NETIFYD-TO-NDPID.md b/DOCS/MIGRATION-NETIFYD-TO-NDPID.md new file mode 100644 index 0000000..63e691b --- /dev/null +++ b/DOCS/MIGRATION-NETIFYD-TO-NDPID.md @@ -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* diff --git a/package/secubox/luci-app-ndpid/Makefile b/package/secubox/luci-app-ndpid/Makefile new file mode 100644 index 0000000..2d0e265 --- /dev/null +++ b/package/secubox/luci-app-ndpid/Makefile @@ -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 + +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 diff --git a/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/ndpid/api.js b/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/ndpid/api.js new file mode 100644 index 0000000..bd5450e --- /dev/null +++ b/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/ndpid/api.js @@ -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'; + } +}; diff --git a/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/ndpid/dashboard.css b/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/ndpid/dashboard.css new file mode 100644 index 0000000..607e0cd --- /dev/null +++ b/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/ndpid/dashboard.css @@ -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; +} diff --git a/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/view/ndpid/dashboard.js b/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/view/ndpid/dashboard.js new file mode 100644 index 0000000..e35783b --- /dev/null +++ b/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/view/ndpid/dashboard.js @@ -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 = '' + + (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 +}); diff --git a/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/view/ndpid/flows.js b/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/view/ndpid/flows.js new file mode 100644 index 0000000..2779986 --- /dev/null +++ b/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/view/ndpid/flows.js @@ -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 +}); diff --git a/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/view/ndpid/settings.js b/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/view/ndpid/settings.js new file mode 100644 index 0000000..205e0f4 --- /dev/null +++ b/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/view/ndpid/settings.js @@ -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(); + } +}); diff --git a/package/secubox/luci-app-ndpid/root/etc/init.d/ndpid-compat b/package/secubox/luci-app-ndpid/root/etc/init.d/ndpid-compat new file mode 100644 index 0000000..c9a1e93 --- /dev/null +++ b/package/secubox/luci-app-ndpid/root/etc/init.d/ndpid-compat @@ -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" +} diff --git a/package/secubox/luci-app-ndpid/root/usr/bin/ndpid-collector b/package/secubox/luci-app-ndpid/root/usr/bin/ndpid-collector new file mode 100644 index 0000000..6d2c670 --- /dev/null +++ b/package/secubox/luci-app-ndpid/root/usr/bin/ndpid-collector @@ -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 diff --git a/package/secubox/luci-app-ndpid/root/usr/bin/ndpid-compat b/package/secubox/luci-app-ndpid/root/usr/bin/ndpid-compat new file mode 100644 index 0000000..636d800 --- /dev/null +++ b/package/secubox/luci-app-ndpid/root/usr/bin/ndpid-compat @@ -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 diff --git a/package/secubox/luci-app-ndpid/root/usr/bin/ndpid-flow-actions b/package/secubox/luci-app-ndpid/root/usr/bin/ndpid-flow-actions new file mode 100644 index 0000000..651a451 --- /dev/null +++ b/package/secubox/luci-app-ndpid/root/usr/bin/ndpid-flow-actions @@ -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 diff --git a/package/secubox/luci-app-ndpid/root/usr/libexec/rpcd/luci.ndpid b/package/secubox/luci-app-ndpid/root/usr/libexec/rpcd/luci.ndpid new file mode 100644 index 0000000..cddcfc4 --- /dev/null +++ b/package/secubox/luci-app-ndpid/root/usr/libexec/rpcd/luci.ndpid @@ -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 diff --git a/package/secubox/luci-app-ndpid/root/usr/share/luci/menu.d/luci-app-ndpid.json b/package/secubox/luci-app-ndpid/root/usr/share/luci/menu.d/luci-app-ndpid.json new file mode 100644 index 0000000..fbf4f69 --- /dev/null +++ b/package/secubox/luci-app-ndpid/root/usr/share/luci/menu.d/luci-app-ndpid.json @@ -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" + } + } +} diff --git a/package/secubox/luci-app-ndpid/root/usr/share/rpcd/acl.d/luci-app-ndpid.json b/package/secubox/luci-app-ndpid/root/usr/share/rpcd/acl.d/luci-app-ndpid.json new file mode 100644 index 0000000..a81f287 --- /dev/null +++ b/package/secubox/luci-app-ndpid/root/usr/share/rpcd/acl.d/luci-app-ndpid.json @@ -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"] + } + } +} diff --git a/package/secubox/secubox-app-ndpid/Makefile b/package/secubox/secubox-app-ndpid/Makefile new file mode 100644 index 0000000..4edf6e4 --- /dev/null +++ b/package/secubox/secubox-app-ndpid/Makefile @@ -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 +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)) diff --git a/package/secubox/secubox-app-ndpid/files/functions.sh b/package/secubox/secubox-app-ndpid/files/functions.sh new file mode 100644 index 0000000..00d4608 --- /dev/null +++ b/package/secubox/secubox-app-ndpid/files/functions.sh @@ -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] $*" +} diff --git a/package/secubox/secubox-app-ndpid/files/ndpid.conf b/package/secubox/secubox-app-ndpid/files/ndpid.conf new file mode 100644 index 0000000..c1c8f9c --- /dev/null +++ b/package/secubox/secubox-app-ndpid/files/ndpid.conf @@ -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 diff --git a/package/secubox/secubox-app-ndpid/files/ndpid.config b/package/secubox/secubox-app-ndpid/files/ndpid.config new file mode 100644 index 0000000..670c97e --- /dev/null +++ b/package/secubox/secubox-app-ndpid/files/ndpid.config @@ -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' diff --git a/package/secubox/secubox-app-ndpid/files/ndpid.init b/package/secubox/secubox-app-ndpid/files/ndpid.init new file mode 100644 index 0000000..5565cf8 --- /dev/null +++ b/package/secubox/secubox-app-ndpid/files/ndpid.init @@ -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" +} diff --git a/package/secubox/secubox-app-ndpid/files/ndpisrvd.init b/package/secubox/secubox-app-ndpid/files/ndpisrvd.init new file mode 100644 index 0000000..ec831d4 --- /dev/null +++ b/package/secubox/secubox-app-ndpid/files/ndpisrvd.init @@ -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" +}