#!/bin/bash ################################################################################ # Script Name: iptables-blocklist-metrics.sh # Version: 2.0 # Description: Prometheus exporter for iptables threat feed blocking metrics # Author: Phil Connor # Contact: contact@mylinux.work # Website: https://mylinux.work # License: MIT ################################################################################ # Ensure PATH includes sbin (for ipset/iptables when run from cron) export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH" # # EXPORTED METRICS: # - iptables_blocklist_info - Exporter metadata # - iptables_blocklist_enabled_feeds - Count of enabled feeds # - iptables_blocklist_ipset_size - IPs per feed ipset (IPv4/v6) # - iptables_blocklist_blocked_total - Block counts per feed (1h, 24h) # - iptables_blocklist_effectiveness - Blocks per 1000 IPs (24h) # - iptables_blocklist_last_update_timestamp - Feed cache file mtime # - iptables_blocklist_cache_age_seconds - Age of feed cache files # - iptables_blocklist_file_size_bytes - Feed parsed file sizes # - iptables_blocklist_ip_version_ratio - IPv4 vs IPv6 distribution per feed # - iptables_blocklist_total_unique_ips - Total unique IPs across all feeds # - iptables_blocklist_total_rules - Total iptables rules # - iptables_blocklist_rule_packets - Packet counts from iptables rules # - iptables_blocklist_rule_bytes - Byte counts from iptables rules # - iptables_blocklist_conntrack_entries - Current conntrack entries # - iptables_blocklist_conntrack_max - Maximum conntrack entries # - iptables_blocklist_conntrack_usage_percent - Conntrack usage percentage # - iptables_blocklist_whitelist_size - Whitelist ipset sizes # - iptables_blocklist_exporter_runtime_seconds - Script execution time CONFIG_DIR="/etc/iptables-threats" CACHE_DIR="$CONFIG_DIR/cache" FEEDS_CONFIG="$CONFIG_DIR/feeds.conf" IPSET_PREFIX="iptables-feed" WHITELIST_IPSET="iptables-whitelist" WHITELIST_IPSET_V6="iptables-whitelist-v6" LOG_FILE="/var/log/iptables-threats.log" TEXTFILE_DIR="/var/lib/node_exporter" OUTPUT_FILE="" HTTP_MODE=false HTTP_PORT=9419 SCRIPT_START_TIME=$(date +%s) LOCK_FILE="/var/run/iptables-blocklist-metrics.lock" show_usage() { cat </dev/null | grep '^[0-9a-fA-F.:]' | wc -l 2>/dev/null) echo "${size:-0}" } get_feed_blocks() { local feed="$1" local period="$2" local count count=$(journalctl -k --since "$period" 2>/dev/null | grep "\[THREAT:${feed}\]" | wc -l 2>/dev/null) echo "${count:-0}" } get_feed_blocks_v6() { local feed="$1" local period="$2" local count count=$(journalctl -k --since "$period" 2>/dev/null | grep "\[THREAT-v6:${feed}\]" | wc -l 2>/dev/null) echo "${count:-0}" } get_file_timestamp() { [ -f "$1" ] && stat -c %Y "$1" 2>/dev/null || echo "0" } get_file_size() { [ -f "$1" ] && stat -c %s "$1" 2>/dev/null || echo "0" } get_cache_age() { if [ -f "$1" ]; then echo $(($(date +%s) - $(stat -c %Y "$1" 2>/dev/null || echo 0))) else echo "0" fi } get_iptables_rule_stats() { local chain="$1" local feed="$2" # Extract packet and byte counts from iptables -L -v -n -x (exact numbers, no human-readable K/M/G) iptables -L "$chain" -v -n -x 2>/dev/null | grep "${IPSET_PREFIX}-${feed}" | head -1 | awk '{print $1"|"$2}' } get_total_unique_ips() { local ip_version="$1" local count=0 if [ "$ip_version" = "4" ]; then count=$(cat "$CACHE_DIR/"*-v4.parsed 2>/dev/null | sort -u | wc -l 2>/dev/null) elif [ "$ip_version" = "6" ]; then count=$(cat "$CACHE_DIR/"*-v6.parsed 2>/dev/null | sort -u | wc -l 2>/dev/null) fi echo "${count:-0}" } get_conntrack_count() { if [ -f /proc/sys/net/netfilter/nf_conntrack_count ]; then cat /proc/sys/net/netfilter/nf_conntrack_count else echo "0" fi } get_conntrack_max() { if [ -f /proc/sys/net/netfilter/nf_conntrack_max ]; then cat /proc/sys/net/netfilter/nf_conntrack_max else echo "0" fi } get_ipset_memory() { local ipset_name="$1" local mem mem=$(ipset list "$ipset_name" -t 2>/dev/null | grep "Size in memory:" | awk '{print $4}') echo "${mem:-0}" } get_cache_disk_usage() { if [ -d "$CACHE_DIR" ]; then df -B1 "$CACHE_DIR" 2>/dev/null | tail -1 | awk '{print $3"|"$4"|"$5}' else echo "0|0|0%" fi } get_total_cache_size() { if [ -d "$CACHE_DIR" ]; then du -sb "$CACHE_DIR" 2>/dev/null | awk '{print $1}' else echo "0" fi } acquire_lock() { if [ -f "$LOCK_FILE" ]; then local pid=$(cat "$LOCK_FILE" 2>/dev/null) if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then echo "ERROR: Another instance is already running (PID: $pid)" >&2 exit 1 else echo "Removing stale lock file" >&2 rm -f "$LOCK_FILE" fi fi echo $$ > "$LOCK_FILE" trap cleanup EXIT INT TERM } cleanup() { rm -f "$LOCK_FILE" } generate_metrics() { local start_time=$(date +%s) local current_time=$(date +%s) cat </dev/null || echo 0) # HELP iptables_blocklist_ipset_size Number of IPs per feed ipset # TYPE iptables_blocklist_ipset_size gauge EOF # Only export metrics for ipsets that actually exist for ipset_name in $(ipset list -n 2>/dev/null | grep "^${IPSET_PREFIX}-"); do # Extract feed name and IP version local feed_name="${ipset_name#${IPSET_PREFIX}-}" local ip_version="4" if [[ "$feed_name" =~ -v6$ ]]; then feed_name="${feed_name%-v6}" ip_version="6" fi # Get status from config local status="disabled" if grep -q "^1|${feed_name}|" "$FEEDS_CONFIG" 2>/dev/null; then status="enabled" fi local size=$(get_ipset_size "$ipset_name") echo "iptables_blocklist_ipset_size{feed=\"$feed_name\",ip_version=\"$ip_version\",status=\"$status\"} $size" done cat </dev/null; then effectiveness_v4=$(awk "BEGIN {printf \"%.2f\", ($blocks_v4 / $ipset_size) * 1000}" 2>/dev/null || echo "0") effectiveness_v6=$(awk "BEGIN {printf \"%.2f\", ($blocks_v6 / $ipset_size) * 1000}" 2>/dev/null || echo "0") else effectiveness_v4="0" effectiveness_v6="0" fi echo "iptables_blocklist_effectiveness{feed=\"$name\",ip_version=\"4\"} $effectiveness_v4" echo "iptables_blocklist_effectiveness{feed=\"$name\",ip_version=\"6\"} $effectiveness_v6" done < "$FEEDS_CONFIG" fi # Feed update/cache metrics cat </dev/null; then ratio_v4=$(awk "BEGIN {printf \"%.4f\", $v4_size / $total}" 2>/dev/null || echo "0") ratio_v6=$(awk "BEGIN {printf \"%.4f\", $v6_size / $total}" 2>/dev/null || echo "0") else ratio_v4="0" ratio_v6="0" fi echo "iptables_blocklist_ip_version_ratio{feed=\"$name\",version=\"4\"} $ratio_v4" echo "iptables_blocklist_ip_version_ratio{feed=\"$name\",version=\"6\"} $ratio_v6" done < "$FEEDS_CONFIG" fi # Total metrics cat </dev/null | wc -l) # HELP iptables_blocklist_rule_packets Packet counts from iptables rules # TYPE iptables_blocklist_rule_packets counter EOF if [ -f "$FEEDS_CONFIG" ]; then while IFS='|' read -r enabled name url type description; do [[ "$enabled" =~ ^#.*$ ]] && continue [[ -z "$enabled" ]] && continue [ "$enabled" != "1" ] && continue local stats_log stats_drop packets_log bytes_log packets_drop bytes_drop stats_log=$(iptables -L INPUT -v -n -x 2>/dev/null | grep "${IPSET_PREFIX}-${name}" | grep LOG | head -1 | awk '{print $1"|"$2}') stats_drop=$(iptables -L INPUT -v -n -x 2>/dev/null | grep "${IPSET_PREFIX}-${name}" | grep DROP | head -1 | awk '{print $1"|"$2}') if [ -n "$stats_log" ]; then packets_log=$(echo "$stats_log" | cut -d'|' -f1) bytes_log=$(echo "$stats_log" | cut -d'|' -f2) echo "iptables_blocklist_rule_packets{feed=\"$name\",ip_version=\"4\",action=\"log\"} ${packets_log:-0}" fi if [ -n "$stats_drop" ]; then packets_drop=$(echo "$stats_drop" | cut -d'|' -f1) bytes_drop=$(echo "$stats_drop" | cut -d'|' -f2) echo "iptables_blocklist_rule_packets{feed=\"$name\",ip_version=\"4\",action=\"drop\"} ${packets_drop:-0}" fi done < "$FEEDS_CONFIG" fi cat </dev/null | grep "${IPSET_PREFIX}-${name}" | grep LOG | head -1 | awk '{print $1"|"$2}') stats_drop=$(iptables -L INPUT -v -n -x 2>/dev/null | grep "${IPSET_PREFIX}-${name}" | grep DROP | head -1 | awk '{print $1"|"$2}') if [ -n "$stats_log" ]; then packets_log=$(echo "$stats_log" | cut -d'|' -f1) bytes_log=$(echo "$stats_log" | cut -d'|' -f2) echo "iptables_blocklist_rule_bytes{feed=\"$name\",ip_version=\"4\",action=\"log\"} ${bytes_log:-0}" fi if [ -n "$stats_drop" ]; then packets_drop=$(echo "$stats_drop" | cut -d'|' -f1) bytes_drop=$(echo "$stats_drop" | cut -d'|' -f2) echo "iptables_blocklist_rule_bytes{feed=\"$name\",ip_version=\"4\",action=\"drop\"} ${bytes_drop:-0}" fi done < "$FEEDS_CONFIG" fi cat </dev/null; then conntrack_usage=$(awk "BEGIN {printf \"%.2f\", ($conntrack_count / $conntrack_max) * 100}" 2>/dev/null || echo "0") else conntrack_usage="0" fi # Cache disk metrics local disk_info cache_size disk_used disk_avail disk_pct disk_info=$(get_cache_disk_usage) cache_size=$(get_total_cache_size) disk_used=$(echo "$disk_info" | cut -d'|' -f1) disk_avail=$(echo "$disk_info" | cut -d'|' -f2) disk_pct=$(echo "$disk_info" | cut -d'|' -f3 | tr -d '%') cat </dev/null 2>&1; then echo "ERROR: netcat (nc) is required for HTTP mode" echo "Install with: yum install nmap-ncat (RHEL/CentOS)" echo " or: apt install netcat (Debian/Ubuntu)" exit 1 fi while true; do { read -r request if [[ "$request" =~ ^GET\ /metrics ]]; then echo -e "HTTP/1.1 200 OK\r\nContent-Type: text/plain; version=0.0.4\r\n\r" generate_metrics else echo -e "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r" echo "

iptables Blocklist Metrics Exporter

" echo "

Per-feed threat blocking statistics

" echo "

Metrics

" fi } | nc -l -p "$HTTP_PORT" -q 1 2>/dev/null done } main() { parse_args "$@" [ ! -d "$CONFIG_DIR" ] && { echo "ERROR: $CONFIG_DIR not found. Run iptables-blocklists.sh first" >&2; exit 1; } # Prevent multiple instances (skip for HTTP mode as it should run continuously) [ "$HTTP_MODE" != true ] && acquire_lock if [ "$HTTP_MODE" = true ]; then run_http_server elif [ -n "$OUTPUT_FILE" ]; then # Ensure output directory exists mkdir -p "$(dirname "$OUTPUT_FILE")" # Create temp file in /tmp (not in node_exporter directory!) # This prevents node_exporter from seeing partial writes local temp_file=$(mktemp /tmp/iptables_metrics.XXXXXX) # Generate metrics to temp file generate_metrics > "$temp_file" # FORCE NEW INODE: Delete old file first, then move # Some node_exporter versions cache file descriptors rm -f "$OUTPUT_FILE" # Move temp file to final location mv "$temp_file" "$OUTPUT_FILE" # Ensure node_exporter user can read it chmod 644 "$OUTPUT_FILE" # Force filesystem sync (optional but helps) sync else generate_metrics fi } main "$@"