#!/bin/sh # nDPId to Netifyd Compatibility Layer # Translates nDPId events to Netifyd-compatible format with enhanced detection # 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" APPS_FILE="/tmp/ndpid-apps.json" STATS_FILE="/tmp/ndpid-stats.json" UPDATE_INTERVAL=1 MAX_FLOWS=500 MAX_APPS=100 # State directory 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" FLOWS_TMP="$STATE_DIR/flows" APPS_TMP="$STATE_DIR/apps" # 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" echo "[]" > "$FLOWS_TMP" echo "{}" > "$APPS_TMP" } # Counter operations inc_counter() { local file="$1" local val=$(cat "$file" 2>/dev/null || echo 0) echo $((val + 1)) > "$file" } 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() { cat "$1" 2>/dev/null || echo 0 } # Update interface stats update_iface_stats() { local iface="$1" local proto="$2" local bytes="$3" [ -z "$iface" ] && return local stats=$(cat "$STATS_FILE_TMP" 2>/dev/null || echo "{}") if command -v jq >/dev/null 2>&1; then stats=$(echo "$stats" | jq --arg iface "$iface" --arg proto "$proto" --argjson bytes "${bytes:-0}" ' .[$iface] //= {"ip_bytes": 0, "wire_bytes": 0, "tcp": 0, "udp": 0, "icmp": 0} | .[$iface].ip_bytes += $bytes | .[$iface].wire_bytes += $bytes | if $proto == "tcp" or $proto == "6" then .[$iface].tcp += 1 elif $proto == "udp" or $proto == "17" then .[$iface].udp += 1 elif $proto == "icmp" or $proto == "1" then .[$iface].icmp += 1 else . end ') echo "$stats" > "$STATS_FILE_TMP" fi } # Update application stats update_app_stats() { local app="$1" local category="$2" local bytes="$3" [ -z "$app" ] && return if command -v jq >/dev/null 2>&1; then local apps=$(cat "$APPS_TMP" 2>/dev/null || echo "{}") apps=$(echo "$apps" | jq --arg app "$app" --arg cat "${category:-Unknown}" --argjson bytes "${bytes:-0}" ' .[$app] //= {"name": $app, "category": $cat, "flows": 0, "bytes": 0} | .[$app].flows += 1 | .[$app].bytes += $bytes ') echo "$apps" > "$APPS_TMP" fi } # Add flow to list add_flow() { local json="$1" if command -v jq >/dev/null 2>&1; then local flow_info=$(echo "$json" | jq -c '{ id: .flow_id, src_ip: .src_ip, src_port: .src_port, dst_ip: .dst_ip, dst_port: .dst_port, proto: .l4_proto, app: (.ndpi.app_proto // .ndpi.proto // "Unknown"), category: (.ndpi.category // "Unknown"), hostname: (.ndpi.hostname // .flow_dst_hostname // null), confidence: (.ndpi.confidence // "Unknown"), risk: (.ndpi.flow_risk // []), bytes_rx: (.flow_src_tot_l4_payload_len // 0), bytes_tx: (.flow_dst_tot_l4_payload_len // 0), packets: ((.flow_src_packets_processed // 0) + (.flow_dst_packets_processed // 0)), first_seen: .flow_first_seen, last_seen: .flow_last_seen, state: "active", iface: .source }' 2>/dev/null) [ -z "$flow_info" ] && return local flows=$(cat "$FLOWS_TMP" 2>/dev/null || echo "[]") flows=$(echo "$flows" | jq --argjson flow "$flow_info" --argjson max "$MAX_FLOWS" ' [. | .[] | select(.id != $flow.id)] + [$flow] | .[-$max:] ') echo "$flows" > "$FLOWS_TMP" fi } # Update existing flow update_flow() { local json="$1" local flow_id=$(echo "$json" | jsonfilter -e '@.flow_id' 2>/dev/null) [ -z "$flow_id" ] && return if command -v jq >/dev/null 2>&1; then local update_info=$(echo "$json" | jq -c '{ bytes_rx: (.flow_src_tot_l4_payload_len // 0), bytes_tx: (.flow_dst_tot_l4_payload_len // 0), packets: ((.flow_src_packets_processed // 0) + (.flow_dst_packets_processed // 0)), last_seen: .flow_last_seen }' 2>/dev/null) local flows=$(cat "$FLOWS_TMP" 2>/dev/null || echo "[]") flows=$(echo "$flows" | jq --arg id "$flow_id" --argjson update "$update_info" ' map(if .id == ($id | tonumber) then . + $update else . end) ') echo "$flows" > "$FLOWS_TMP" fi } # Mark flow as ended end_flow() { local json="$1" local flow_id=$(echo "$json" | jsonfilter -e '@.flow_id' 2>/dev/null) [ -z "$flow_id" ] && return if command -v jq >/dev/null 2>&1; then local flows=$(cat "$FLOWS_TMP" 2>/dev/null || echo "[]") flows=$(echo "$flows" | jq --arg id "$flow_id" ' map(if .id == ($id | tonumber) then .state = "ended" else . end) ') echo "$flows" > "$FLOWS_TMP" fi } # Process a single nDPId event process_event() { local raw="$1" # Strip 5-digit length prefix if present local json="$raw" if echo "$raw" | grep -q '^[0-9]\{5\}'; then json="${raw:5}" fi # 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) # Extract common fields 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)) case "$event_name" in new) inc_counter "$FLOW_COUNT_FILE" inc_counter "$FLOWS_ACTIVE_FILE" add_flow "$json" ;; detected|guessed|detection-update) # Extract application info local app=$(echo "$json" | jsonfilter -e '@.ndpi.app_proto' 2>/dev/null) [ -z "$app" ] && app=$(echo "$json" | jsonfilter -e '@.ndpi.proto' 2>/dev/null) local category=$(echo "$json" | jsonfilter -e '@.ndpi.category' 2>/dev/null) # Update stats [ -n "$iface" ] && update_iface_stats "$iface" "$proto" "$total_bytes" [ -n "$app" ] && update_app_stats "$app" "$category" "$total_bytes" # Update flow details add_flow "$json" ;; update) update_flow "$json" [ -n "$iface" ] && update_iface_stats "$iface" "$proto" "$total_bytes" ;; end|idle) dec_counter "$FLOWS_ACTIVE_FILE" end_flow "$json" ;; esac } # Generate status files 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)) # Generate main status file 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" # Generate flows file cp "$FLOWS_TMP" "$FLOWS_FILE" 2>/dev/null # Generate apps file (sorted by bytes) local apps=$(cat "$APPS_TMP" 2>/dev/null || echo "{}") echo "$apps" | jq '[.[] | select(.name != null)] | sort_by(-.bytes) | .[0:100]' > "$APPS_FILE" 2>/dev/null 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 } # Cleanup old ended flows cleanup_flows() { if command -v jq >/dev/null 2>&1; then local flows=$(cat "$FLOWS_TMP" 2>/dev/null || echo "[]") local now=$(date +%s) # Keep active flows and ended flows from last 5 minutes flows=$(echo "$flows" | jq --argjson now "$now" ' [.[] | select(.state == "active" or (.last_seen != null and ($now - .last_seen) < 300))] ') echo "$flows" > "$FLOWS_TMP" fi } # Main loop main() { START_TIME=$(date +%s) logger -t ndpid-compat "Starting nDPId compatibility layer (enhanced)" # Initialize state init_state # Check for dependencies if ! command -v socat >/dev/null 2>&1; then logger -t ndpid-compat "WARNING: socat not found, using nc fallback" USE_NC=1 fi if ! command -v jq >/dev/null 2>&1; then logger -t ndpid-compat "WARNING: jq not found, detailed stats disabled" 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=$! # Background cleanup ( while true; do sleep 60 cleanup_flows done ) & CLEANUP_PID=$! trap "kill $STATUS_PID $CLEANUP_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 modes case "$1" in -h|--help) echo "Usage: $0 [-d|--daemon]" echo " Enhanced nDPId to Netifyd compatibility layer" echo " Captures detailed flow and application information" exit 0 ;; -d|--daemon) main & echo $! > /var/run/ndpid-compat.pid ;; *) main ;; esac