# 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*