#!/bin/sh # shellcheck shell=ash # Variables are populated by sourced SafeShield helpers and ss_load_config(). # shellcheck disable=SC2154 LC_ALL=C export LC_ALL umask 077 SS_STATSD_FUNCTIONS_LIB="${SS_STATSD_FUNCTIONS_LIB:-/lib/functions.sh}" SS_STATSD_CORE_LIB="${SS_STATSD_CORE_LIB:-/usr/lib/safeshield/core.sh}" SS_STATSD_AWK_DIR="${SS_STATSD_AWK_DIR:-/usr/lib/safeshield/statistics}" SS_STATSD_POLL_COMMAND="${SS_STATSD_POLL_COMMAND:-/usr/libexec/safeshield-stats-poll}" SS_STATSD_IDENTITY_COMMAND="${SS_STATSD_IDENTITY_COMMAND:-/usr/libexec/safeshield-stats-identities}" SS_STATSD_PERSISTENT_STATE_FILE="${SS_STATSD_PERSISTENT_STATE_FILE:-/etc/safeshield/statistics-state.tsv}" SS_STATSD_PERSISTENT_JOURNAL_FILE="${SS_STATSD_PERSISTENT_JOURNAL_FILE:-/etc/safeshield/statistics-journal.tsv}" SS_STATSD_SEQUENCE_FILE="${SS_STATSD_SEQUENCE_FILE:-/etc/safeshield/statistics-sequence.tsv}" SS_STATSD_SEQUENCE_RESERVATION_SIZE="${SS_STATSD_SEQUENCE_RESERVATION_SIZE:-4096}" SS_STATSD_UPLOAD_WINDOW_HOURS="${SS_STATSD_UPLOAD_WINDOW_HOURS:-2}" SS_STATSD_PERSISTENT_INTERVAL_S="${SS_STATSD_PERSISTENT_INTERVAL_S:-3600}" SS_STATSD_PERSISTENT_COMPACT_INTERVAL_S="${SS_STATSD_PERSISTENT_COMPACT_INTERVAL_S:-604800}" # shellcheck disable=SC1090 . "$SS_STATSD_FUNCTIONS_LIB" # shellcheck disable=SC1090 . "$SS_STATSD_CORE_LIB" stats_poller_pid='' stats_awk_pid='' stats_fifo='' stats_lock_dir='' stats_lock_owned=0 stats_new_generation_id() { local generation_id="${SS_STATSD_GENERATION_ID:-}" if [ -n "$generation_id" ]; then printf '%s\n' "$generation_id" return 0 fi if [ -r /proc/sys/kernel/random/uuid ]; then IFS= read -r generation_id /dev/null } stats_cleanup() { trap - EXIT HUP INT TERM # Stop the low-frequency UBus poller first. awk then receives EOF and runs # its END block, which performs the final snapshot and persistence flush. if stats_process_running "$stats_poller_pid"; then kill "$stats_poller_pid" 2>/dev/null || true fi [ -n "$stats_poller_pid" ] && wait "$stats_poller_pid" 2>/dev/null || true stats_poller_pid='' [ -n "$stats_awk_pid" ] && wait "$stats_awk_pid" 2>/dev/null || true stats_awk_pid='' [ -n "$stats_fifo" ] && rm -f "$stats_fifo" if [ "$stats_lock_owned" -eq 1 ] && [ -n "$stats_lock_dir" ]; then rm -rf "$stats_lock_dir" fi } stats_terminate() { local rc="${1:-0}" stats_cleanup exit "$rc" } stats_acquire_lock() { local owner_pid='' stats_lock_dir="${SS_STATISTICS_DIR}/collector.lock" if mkdir "$stats_lock_dir" 2>/dev/null; then stats_lock_owned=1 printf '%s\n' "$$" >"${stats_lock_dir}/pid" return 0 fi if [ -r "${stats_lock_dir}/pid" ]; then owner_pid="$(cat "${stats_lock_dir}/pid" 2>/dev/null || true)" fi if stats_process_running "$owner_pid"; then log_warn "Statistics collector is already running (pid=${owner_pid})" return 1 fi log_warn 'Removing stale statistics collector lock' rm -rf "$stats_lock_dir" || return 1 mkdir "$stats_lock_dir" || return 1 stats_lock_owned=1 printf '%s\n' "$$" >"${stats_lock_dir}/pid" return 0 } stats_poll_loop() { local snapshot_interval="$1" local poll_command="$2" local sleep_pid='' trap 'if [ -n "$sleep_pid" ]; then kill "$sleep_pid" 2>/dev/null || true; fi; exit 0' HUP INT TERM while :; do if ! "$poll_command"; then printf 'error\tdnsmasq_ubus_poll_failed\n' fi sleep "$snapshot_interval" & sleep_pid=$! wait "$sleep_pid" 2>/dev/null || true sleep_pid='' done } stats_run_collector() { local snapshot_interval="$1" local retention_hours="$2" local lease_file="$3" local persistent_state_file="$4" local persistent_journal_file="$5" local sequence_file="$6" local generation_id ipv6_neigh_command='' ipv6_identity_command='' ipv6_identity_file="${SS_STATSD_IPV6_IDENTITY_FILE:-}" force_rebaseline=0 rc=0 if [ -f "${SS_STATISTICS_REBASELINE_FILE}" ]; then force_rebaseline=1 fi # This is only a candidate for a brand-new dataset. The collector modules keep # the generation restored from tmpfs or persistent state when one already exists. generation_id="$(stats_new_generation_id)" if command_exists ip; then ipv6_neigh_command='ip -6 neigh show 2>/dev/null' fi if [ -z "$ipv6_identity_file" ] && [ -x "$SS_STATSD_IDENTITY_COMMAND" ]; then ipv6_identity_command="$SS_STATSD_IDENTITY_COMMAND 2>/dev/null" fi stats_fifo="${SS_STATISTICS_DIR}/events.$$" rm -f "$stats_fifo" mkfifo "$stats_fifo" || { log_error "Failed to create statistics FIFO: ${stats_fifo}" return 1 } stats_poll_loop "$snapshot_interval" "$SS_STATSD_POLL_COMMAND" >"$stats_fifo" & stats_poller_pid=$! awk \ -v state_file="${SS_STATISTICS_STATE_FILE}" \ -v json_file="${SS_STATISTICS_JSON_FILE}" \ -v upload_json_file="${SS_STATISTICS_UPLOAD_JSON_FILE}" \ -v upload_window_hours="${SS_STATSD_UPLOAD_WINDOW_HOURS}" \ -v persistent_state_file="${persistent_state_file}" \ -v persistent_journal_file="${persistent_journal_file}" \ -v sequence_file="${sequence_file}" \ -v snapshot_seq_reservation_size="${SS_STATSD_SEQUENCE_RESERVATION_SIZE}" \ -v persistent_interval="${SS_STATSD_PERSISTENT_INTERVAL_S}" \ -v persistent_compact_interval="${SS_STATSD_PERSISTENT_COMPACT_INTERVAL_S}" \ -v snapshot_interval="${snapshot_interval}" \ -v retention_hours="${retention_hours}" \ -v lease_file="${lease_file}" \ -v ipv6_neigh_command="${ipv6_neigh_command}" \ -v ipv6_identity_file="${ipv6_identity_file}" \ -v ipv6_identity_command="${ipv6_identity_command}" \ -v lease_refresh_interval=60 \ -v identity_cache_ttl=60 \ -v max_devices=128 \ -v generation_seed="${generation_id}" \ -v force_rebaseline="${force_rebaseline}" \ -v rebaseline_file="${SS_STATISTICS_REBASELINE_FILE}" \ -f "$SS_STATSD_AWK_DIR/00-common.awk" \ -f "$SS_STATSD_AWK_DIR/10-recovery.awk" \ -f "$SS_STATSD_AWK_DIR/20-identity.awk" \ -f "$SS_STATSD_AWK_DIR/30-aggregate.awk" \ -f "$SS_STATSD_AWK_DIR/40-persistence.awk" \ -f "$SS_STATSD_AWK_DIR/50-output.awk" \ -f "$SS_STATSD_AWK_DIR/90-main.awk" \ <"$stats_fifo" & stats_awk_pid=$! wait "$stats_awk_pid" || rc=$? stats_awk_pid='' if stats_process_running "$stats_poller_pid"; then kill "$stats_poller_pid" 2>/dev/null || true fi wait "$stats_poller_pid" 2>/dev/null || true stats_poller_pid='' return "$rc" } main() { local snapshot_interval configured_snapshot_interval retention_hours lease_file configured_lease_file local persistent_state_file persistent_journal_file sequence_file persistence_mode rc=0 ss_load_config || return 1 [ "${ss_enabled}" = "1" ] || return 0 [ "${ss_statistics_enabled}" = "1" ] || return 0 if ! ss_dnsmasq_safeshield_block_supported; then log_warn 'SafeShield statistics collector is disabled because dnsmasq does not provide the SafeShield extension' return 0 fi [ -x "$SS_STATSD_POLL_COMMAND" ] || { log_error "SafeShield statistics poll command is unavailable: ${SS_STATSD_POLL_COMMAND}" return 1 } command_exists mkfifo || { log_error 'mkfifo is required for SafeShield statistics' return 1 } persistent_state_file="${SS_STATSD_PERSISTENT_STATE_FILE}" persistent_journal_file="${SS_STATSD_PERSISTENT_JOURNAL_FILE}" sequence_file="${SS_STATSD_SEQUENCE_FILE}" persistence_mode='journal' if ! ss_statistics_persistence_enabled; then persistent_state_file='' persistent_journal_file='' sequence_file='' persistence_mode='volatile' fi mkdir -p "${SS_STATISTICS_DIR}" || return 1 if [ -n "$persistent_state_file" ]; then mkdir -p "${persistent_state_file%/*}" || return 1 fi if [ -n "$persistent_journal_file" ]; then mkdir -p "${persistent_journal_file%/*}" || return 1 fi if [ -n "$sequence_file" ]; then mkdir -p "${sequence_file%/*}" || return 1 fi stats_acquire_lock || return 0 trap 'stats_cleanup' EXIT trap 'stats_terminate 129' HUP trap 'stats_terminate 130' INT trap 'stats_terminate 143' TERM configured_snapshot_interval="${ss_statistics_snapshot_interval_s}" snapshot_interval="$(ss_statistics_effective_snapshot_interval "$configured_snapshot_interval")" retention_hours="${ss_statistics_retention_hours}" lease_file="${SS_STATSD_DHCP_LEASE_FILE:-/tmp/dhcp.leases}" if [ -z "${SS_STATSD_DHCP_LEASE_FILE:-}" ] && command_exists uci; then configured_lease_file="$(uci -q get dhcp.@dnsmasq[0].leasefile 2>/dev/null || true)" [ -n "$configured_lease_file" ] && lease_file="$configured_lease_file" fi log_info "Starting statistics collector (source=dnsmasq-ubus snapshot=${snapshot_interval}s configured_snapshot=${configured_snapshot_interval}s retention=${retention_hours}h persistence=${persistence_mode} journal=${SS_STATSD_PERSISTENT_INTERVAL_S}s compact=${SS_STATSD_PERSISTENT_COMPACT_INTERVAL_S}s)" stats_run_collector \ "$snapshot_interval" \ "$retention_hours" \ "$lease_file" \ "$persistent_state_file" \ "$persistent_journal_file" \ "$sequence_file" || rc=$? return "$rc" } main "$@"