#!/bin/sh # # vortex-firewall - DNS-level threat blocking with ×47 impact # # Block threats at DNS resolution BEFORE any connection is established. # Each DNS block prevents ~47 malicious connections (C2 beacon rate × window). # # Usage: # vortex-firewall intel Threat intelligence management # vortex-firewall stats Show blocking statistics # vortex-firewall sinkhole Sinkhole server management # vortex-firewall mesh Mesh threat sharing # vortex-firewall start|stop|status Service control # VERSION="1.0.0" NAME="vortex-firewall" # Directories VAR_DIR="/var/lib/vortex-firewall" CACHE_DIR="/tmp/vortex-firewall" FEEDS_DIR="$VAR_DIR/feeds" BLOCKLIST_DB="$VAR_DIR/blocklist.db" BLOCKLIST_HOSTS="$VAR_DIR/sinkhole.hosts" DNSMASQ_CONF="/etc/dnsmasq.d/vortex-firewall.conf" STATS_FILE="$VAR_DIR/stats.json" CONFIG_FILE="/etc/config/vortex-firewall" # Sinkhole IP (internal, not routed) SINKHOLE_IP="192.168.255.253" # Feed URLs FEED_URLHAUS="https://urlhaus.abuse.ch/downloads/hostfile/" FEED_FEODO="https://feodotracker.abuse.ch/downloads/ipblocklist.txt" FEED_PHISHTANK="http://data.phishtank.com/data/online-valid.csv" FEED_OPENPHISH="https://openphish.com/feed.txt" FEED_THREATFOX="https://threatfox.abuse.ch/downloads/hostfile/" # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' log() { echo -e "${GREEN}[VORTEX]${NC} $1"; } warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } error() { echo -e "${RED}[ERROR]${NC} $1"; } info() { echo -e "${CYAN}[INFO]${NC} $1"; } # ============================================================================ # Initialization # ============================================================================ init_dirs() { mkdir -p "$VAR_DIR" "$CACHE_DIR" "$FEEDS_DIR" [ -f "$STATS_FILE" ] || echo '{"blocks":0,"queries":0,"domains":0,"last_update":""}' > "$STATS_FILE" } init_db() { if [ ! -f "$BLOCKLIST_DB" ]; then log "Initializing blocklist database..." sqlite3 "$BLOCKLIST_DB" </dev/null; then # Extract domains from hosts file format (127.0.0.1 domain) grep -v '^#' "$feed_file.tmp" 2>/dev/null | awk '{print $2}' | grep -v '^$' | sort -u > "$feed_file" local count=$(wc -l < "$feed_file") rm -f "$feed_file.tmp" sqlite3 "$BLOCKLIST_DB" "INSERT OR REPLACE INTO feeds VALUES ('urlhaus', '$FEED_URLHAUS', datetime('now'), $count, 1);" log "URLhaus: $count domains" return 0 else warn "Failed to update URLhaus feed" return 1 fi } feed_update_openphish() { local feed_file="$FEEDS_DIR/openphish.txt" log "Updating OpenPhish feed..." if curl -sL --connect-timeout 10 --max-time 30 "$FEED_OPENPHISH" -o "$feed_file.tmp" 2>/dev/null; then # Extract domains from URLs grep -v '^#' "$feed_file.tmp" 2>/dev/null | sed 's|https\?://||' | cut -d'/' -f1 | sort -u > "$feed_file" local count=$(wc -l < "$feed_file") rm -f "$feed_file.tmp" sqlite3 "$BLOCKLIST_DB" "INSERT OR REPLACE INTO feeds VALUES ('openphish', '$FEED_OPENPHISH', datetime('now'), $count, 1);" log "OpenPhish: $count domains" return 0 else warn "Failed to update OpenPhish feed" return 1 fi } feed_update_threatfox() { local feed_file="$FEEDS_DIR/threatfox.txt" log "Updating ThreatFox feed..." if curl -sL --connect-timeout 10 --max-time 60 "$FEED_THREATFOX" -o "$feed_file.tmp" 2>/dev/null; then # Extract domains from hosts file format (127.0.0.1 domain) grep -v '^#' "$feed_file.tmp" 2>/dev/null | awk '{print $2}' | grep -v '^$' | sort -u > "$feed_file" local count=$(wc -l < "$feed_file") rm -f "$feed_file.tmp" sqlite3 "$BLOCKLIST_DB" "INSERT OR REPLACE INTO feeds VALUES ('threatfox', '$FEED_THREATFOX', datetime('now'), $count, 1);" log "ThreatFox: $count domains" return 0 else warn "Failed to update ThreatFox feed" return 1 fi } feed_import_dnsguard() { local dnsguard_dir="/var/lib/dns-guard" local dnsguard_list="$dnsguard_dir/threat_domains.txt" local dnsguard_alerts="$dnsguard_dir/alerts.json" local feed_file="$FEEDS_DIR/dnsguard.txt" log "Importing DNS Guard detections..." # Phase 3: Enhanced import with metadata from alerts.json if [ -f "$dnsguard_alerts" ] && [ -s "$dnsguard_alerts" ]; then log "Reading DNS Guard alerts with metadata..." # Parse alerts.json and import with proper threat types and confidence local imported=0 local now=$(date -Iseconds) # Build SQL import from alerts local sql_file="/tmp/vortex-dnsguard-import.sql" echo "BEGIN TRANSACTION;" > "$sql_file" # Read each alert and extract domain, type, confidence jsonfilter -i "$dnsguard_alerts" -e '@[*]' 2>/dev/null | while read -r alert; do local domain=$(echo "$alert" | jsonfilter -e '@.domain' 2>/dev/null | tr -d '\n\r') local threat_type=$(echo "$alert" | jsonfilter -e '@.type' 2>/dev/null | tr -d '\n\r') local confidence=$(echo "$alert" | jsonfilter -e '@.confidence' 2>/dev/null | tr -d '\n\r') # Skip rate anomalies with wildcard domains [ "$domain" = "*" ] && continue [ -z "$domain" ] && continue # Default values [ -z "$threat_type" ] && threat_type="ai_detected" [ -z "$confidence" ] && confidence=80 # Map DNS Guard types to Vortex threat types case "$threat_type" in dga) threat_type="dga" ;; tunneling) threat_type="dns_tunnel" ;; known_bad) threat_type="malware" ;; tld_anomaly) threat_type="suspicious_tld" ;; rate_anomaly) threat_type="rate_anomaly" ;; esac # Escape for SQL domain=$(echo "$domain" | sed "s/'/''/g") echo "INSERT OR REPLACE INTO domains (domain, threat_type, confidence, source, first_seen, last_seen, blocked) VALUES ('$domain', '$threat_type', $confidence, 'dnsguard', '$now', '$now', 1);" >> "$sql_file" imported=$((imported + 1)) done echo "COMMIT;" >> "$sql_file" # Execute import sqlite3 "$BLOCKLIST_DB" < "$sql_file" 2>/dev/null rm -f "$sql_file" # Also copy plaintext list for dnsmasq [ -f "$dnsguard_list" ] && cp "$dnsguard_list" "$feed_file" local count=$(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(*) FROM domains WHERE source='dnsguard';" 2>/dev/null || echo 0) sqlite3 "$BLOCKLIST_DB" "INSERT OR REPLACE INTO feeds VALUES ('dnsguard', 'local', datetime('now'), $count, 1);" log "DNS Guard: $count domains (with AI metadata)" return 0 fi # Fallback: basic import from threat_domains.txt if [ -f "$dnsguard_list" ] && [ -s "$dnsguard_list" ]; then cp "$dnsguard_list" "$feed_file" local count=$(wc -l < "$feed_file") sqlite3 "$BLOCKLIST_DB" "INSERT OR REPLACE INTO feeds VALUES ('dnsguard', 'local', datetime('now'), $count, 1);" log "DNS Guard: $count domains (basic)" return 0 fi info "No DNS Guard detections found" return 0 } intel_update() { init_dirs init_db log "Updating threat intelligence feeds..." echo "" local total=0 # Update each feed feed_update_urlhaus && total=$((total + 1)) feed_update_openphish && total=$((total + 1)) feed_update_threatfox && total=$((total + 1)) feed_import_dnsguard && total=$((total + 1)) echo "" log "Updated $total feeds" # Merge feeds into database intel_merge # Generate dnsmasq blocklist generate_blocklist } is_valid_domain() { local d="$1" # Must contain at least one dot echo "$d" | grep -q '\.' || return 1 # Must have valid TLD (at least 2 chars after last dot) local tld=$(echo "$d" | sed 's/.*\.//') [ ${#tld} -ge 2 ] || return 1 # Must be reasonable length (3-253 chars) [ ${#d} -ge 3 ] && [ ${#d} -le 253 ] || return 1 # Must not start/end with dot or hyphen case "$d" in .*|*.|*-|-*) return 1 ;; esac return 0 } intel_merge() { log "Merging feeds into blocklist..." local now=$(date -Iseconds) local sql_file="/tmp/vortex-import.sql" local imported=0 local skipped=0 # Start transaction echo "BEGIN TRANSACTION;" > "$sql_file" # Import from each feed file for feed_file in "$FEEDS_DIR"/*.txt; do [ -f "$feed_file" ] || continue local feed_name=$(basename "$feed_file" .txt) local threat_type="malware" case "$feed_name" in openphish|phishtank) threat_type="phishing" ;; urlhaus|threatfox) threat_type="malware" ;; dnsguard) threat_type="ai_detected" ;; feodo) threat_type="c2" ;; esac log "Processing $feed_name..." while read -r domain; do [ -z "$domain" ] && continue [ "${domain:0:1}" = "#" ] && continue # Clean domain (inline for speed) domain=$(echo "$domain" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9.-') [ -z "$domain" ] && continue # Quick validation: must have dot and be reasonable length case "$domain" in *.*) ;; *) skipped=$((skipped + 1)); continue ;; esac [ ${#domain} -lt 4 ] && { skipped=$((skipped + 1)); continue; } [ ${#domain} -gt 253 ] && { skipped=$((skipped + 1)); continue; } # Escape single quotes for SQL domain=$(echo "$domain" | sed "s/'/''/g") echo "INSERT OR REPLACE INTO domains (domain, threat_type, source, first_seen, last_seen, blocked) VALUES ('$domain', '$threat_type', '$feed_name', '$now', '$now', 1);" >> "$sql_file" imported=$((imported + 1)) done < "$feed_file" done echo "COMMIT;" >> "$sql_file" # Execute batch import log "Executing batch import ($imported entries)..." sqlite3 "$BLOCKLIST_DB" < "$sql_file" rm -f "$sql_file" local total=$(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(*) FROM domains WHERE blocked=1;") log "Imported: $imported domains, Skipped: $skipped invalid entries" log "Total blocked domains: $total" } generate_blocklist() { # Detect DNS server local dns_server="dnsmasq" if pgrep -f "/usr/sbin/named" >/dev/null 2>&1 || pidof named >/dev/null 2>&1; then dns_server="bind" fi log "Generating blocklist for $dns_server..." local count=$(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(*) FROM domains WHERE blocked=1;") if [ "$dns_server" = "bind" ]; then # Generate BIND RPZ zone generate_bind_rpz "$count" else # Generate dnsmasq hosts file generate_dnsmasq_hosts "$count" fi } generate_bind_rpz() { local count="$1" local rpz_zone="/etc/bind/zones/rpz.vortex.zone" local rpz_conf="/etc/bind/named.conf.vortex" local serial=$(date +%Y%m%d%H) log "Generating BIND RPZ zone ($count domains)..." # Generate RPZ zone file cat > "$rpz_zone" <> "$rpz_zone" echo "*.$domain CNAME ." >> "$rpz_zone" done log "RPZ zone written: $rpz_zone" # Generate BIND config include cat > "$rpz_conf" </dev/null; then log "Adding RPZ policy to BIND config..." # Add response-policy to options block sed -i '/^options {/,/^};/ { /^};/ i\ response-policy { zone "rpz.vortex"; }; }' /etc/bind/named.conf fi # Include vortex config if not already if ! grep -q "named.conf.vortex" /etc/bind/named.conf 2>/dev/null; then echo 'include "/etc/bind/named.conf.vortex";' >> /etc/bind/named.conf fi log "BIND RPZ config written: $rpz_conf" # Reload BIND if [ -x /etc/init.d/named ]; then /etc/init.d/named reload 2>/dev/null || /etc/init.d/named restart 2>/dev/null log "BIND reloaded" fi # Update stats local now=$(date -Iseconds) echo "{\"domains\":$count,\"last_update\":\"$now\",\"blocks\":0,\"queries\":0,\"dns_server\":\"bind\"}" > "$STATS_FILE" } generate_dnsmasq_hosts() { local count="$1" log "Generating dnsmasq blocklist..." # Generate hosts file for sinkhole echo "# Vortex DNS Firewall - Generated $(date)" > "$BLOCKLIST_HOSTS" echo "# Sinkhole IP: $SINKHOLE_IP" >> "$BLOCKLIST_HOSTS" echo "" >> "$BLOCKLIST_HOSTS" sqlite3 -separator ' ' "$BLOCKLIST_DB" \ "SELECT '$SINKHOLE_IP', domain FROM domains WHERE blocked=1;" >> "$BLOCKLIST_HOSTS" log "Generated $count sinkhole entries" # Generate dnsmasq config cat > "$DNSMASQ_CONF" </dev/null log "dnsmasq restarted" fi # Update stats local now=$(date -Iseconds) echo "{\"domains\":$count,\"last_update\":\"$now\",\"blocks\":0,\"queries\":0}" > "$STATS_FILE" } intel_status() { init_dirs init_db echo "" echo -e "${BOLD}Vortex DNS Firewall - Threat Intelligence${NC}" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo -e "${BOLD}Feed Status:${NC}" sqlite3 -column -header "$BLOCKLIST_DB" \ "SELECT name, domain_count as domains, last_update, CASE enabled WHEN 1 THEN 'Active' ELSE 'Disabled' END as status FROM feeds;" echo "" echo -e "${BOLD}Threat Categories:${NC}" sqlite3 -column -header "$BLOCKLIST_DB" \ "SELECT threat_type, COUNT(*) as count FROM domains WHERE blocked=1 GROUP BY threat_type ORDER BY count DESC;" echo "" local total=$(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(*) FROM domains WHERE blocked=1;") echo -e "${BOLD}Total Blocked Domains:${NC} $total" echo "" } intel_search() { local domain="$1" [ -z "$domain" ] && { error "Usage: vortex-firewall intel search "; return 1; } init_db local result=$(sqlite3 -column -header "$BLOCKLIST_DB" \ "SELECT domain, threat_type, confidence, source, first_seen, hit_count FROM domains WHERE domain LIKE '%$domain%' LIMIT 20;") if [ -n "$result" ]; then echo "" echo -e "${RED}BLOCKED${NC} - Domain found in blocklist:" echo "$result" else echo -e "${GREEN}CLEAN${NC} - Domain not in blocklist: $domain" fi } intel_add() { local domain="$1" local reason="${2:-manual}" [ -z "$domain" ] && { error "Usage: vortex-firewall intel add [reason]"; return 1; } init_db domain=$(echo "$domain" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9.-]//g') local now=$(date -Iseconds) sqlite3 "$BLOCKLIST_DB" \ "INSERT OR REPLACE INTO domains (domain, threat_type, confidence, source, first_seen, last_seen, blocked) VALUES ('$domain', '$reason', 100, 'manual', '$now', '$now', 1);" # Add to hosts file immediately echo "$SINKHOLE_IP $domain" >> "$BLOCKLIST_HOSTS" log "Blocked: $domain (reason: $reason)" # Reload dnsmasq killall -HUP dnsmasq 2>/dev/null } intel_remove() { local domain="$1" [ -z "$domain" ] && { error "Usage: vortex-firewall intel remove "; return 1; } init_db sqlite3 "$BLOCKLIST_DB" "UPDATE domains SET blocked=0 WHERE domain='$domain';" # Regenerate blocklist generate_blocklist log "Unblocked: $domain" } # ============================================================================ # Statistics # ============================================================================ show_stats() { init_dirs init_db echo "" echo -e "${BOLD}Vortex DNS Firewall - Statistics${NC}" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" local total_domains=$(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(*) FROM domains WHERE blocked=1;" 2>/dev/null || echo 0) local total_hits=$(sqlite3 "$BLOCKLIST_DB" "SELECT COALESCE(SUM(hit_count),0) FROM domains;" 2>/dev/null || echo 0) local total_events=$(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(*) FROM events;" 2>/dev/null || echo 0) # ×47 Impact calculation local x47_impact=$((total_hits * 47)) echo -e "${BOLD}Blocking Summary:${NC}" echo " Blocked Domains: $total_domains" echo " Total Hits: $total_hits" echo " Sinkhole Events: $total_events" echo "" echo -e "${BOLD}×47 Impact Score:${NC}" echo -e " ${CYAN}$x47_impact${NC} connections prevented" echo " (Each DNS block prevents ~47 malicious connections)" echo "" echo -e "${BOLD}Top Blocked Domains:${NC}" sqlite3 -column "$BLOCKLIST_DB" \ "SELECT domain, hit_count, threat_type FROM domains WHERE hit_count > 0 ORDER BY hit_count DESC LIMIT 10;" 2>/dev/null echo "" echo -e "${BOLD}Threat Distribution:${NC}" sqlite3 "$BLOCKLIST_DB" \ "SELECT threat_type || ': ' || COUNT(*) FROM domains WHERE blocked=1 GROUP BY threat_type ORDER BY COUNT(*) DESC;" 2>/dev/null echo "" } show_x47() { init_db local total_hits=$(sqlite3 "$BLOCKLIST_DB" "SELECT COALESCE(SUM(hit_count),0) FROM domains;" 2>/dev/null || echo 0) local x47_impact=$((total_hits * 47)) local total_domains=$(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(*) FROM domains WHERE blocked=1;" 2>/dev/null || echo 0) echo "" echo -e "${BOLD}╔══════════════════════════════════════════════════╗${NC}" echo -e "${BOLD}║ ×47 VITALITY IMPACT SCORE ║${NC}" echo -e "${BOLD}╠══════════════════════════════════════════════════╣${NC}" echo -e "${BOLD}║${NC} Blocked Domains: ${CYAN}$total_domains${NC}" echo -e "${BOLD}║${NC} DNS Blocks: ${CYAN}$total_hits${NC}" echo -e "${BOLD}║${NC} Connections Prevented: ${GREEN}$x47_impact${NC}" echo -e "${BOLD}║${NC}" echo -e "${BOLD}║${NC} ${YELLOW}Each DNS block = 47 connections stopped${NC}" echo -e "${BOLD}║${NC} ${YELLOW}(C2 beacon rate × infection window)${NC}" echo -e "${BOLD}╚══════════════════════════════════════════════════╝${NC}" echo "" } # ============================================================================ # Sinkhole Server - HTTP/HTTPS Trap for Blocked Domains # ============================================================================ SINKHOLE_PID_HTTP="/var/run/vortex-sinkhole-http.pid" SINKHOLE_PID_HTTPS="/var/run/vortex-sinkhole-https.pid" SINKHOLE_LOG="/var/log/vortex-sinkhole.log" SINKHOLE_HTML="/usr/share/vortex-firewall/sinkhole.html" sinkhole_start() { log "Starting Vortex Sinkhole Server..." init_dirs init_db # Check if sinkhole is enabled in config local enabled=$(uci -q get vortex-firewall.server.enabled) if [ "$enabled" != "1" ]; then warn "Sinkhole server not enabled in config" info "Enable with: uci set vortex-firewall.server.enabled=1 && uci commit" return 1 fi local http_port=$(uci -q get vortex-firewall.server.http_port || echo 80) local https_port=$(uci -q get vortex-firewall.server.https_port || echo 443) # Create sinkhole IP alias if not exists if ! ip addr show dev br-lan 2>/dev/null | grep -q "$SINKHOLE_IP"; then log "Adding sinkhole IP $SINKHOLE_IP to br-lan..." ip addr add "$SINKHOLE_IP/32" dev br-lan 2>/dev/null || true fi # Start HTTP sinkhole if ! pgrep -f "sinkhole-http-handler" >/dev/null 2>&1; then log "Starting HTTP sinkhole on $SINKHOLE_IP:$http_port..." /usr/lib/vortex-firewall/sinkhole-http.sh "$SINKHOLE_IP" "$http_port" & echo $! > "$SINKHOLE_PID_HTTP" fi # Start HTTPS sinkhole (if certificates available) if [ -f "/etc/vortex-firewall/sinkhole.key" ] && [ -f "/etc/vortex-firewall/sinkhole.crt" ]; then if ! pgrep -f "OPENSSL-LISTEN" >/dev/null 2>&1; then log "Starting HTTPS sinkhole on $SINKHOLE_IP:$https_port..." /usr/lib/vortex-firewall/sinkhole-https.sh "$SINKHOLE_IP" "$https_port" & echo $! > "$SINKHOLE_PID_HTTPS" fi else info "HTTPS sinkhole skipped (no certificates)" info "Generate with: vortex-firewall sinkhole gencert" fi log "Sinkhole server started" } sinkhole_stop() { log "Stopping Vortex Sinkhole Server..." # Stop HTTP sinkhole if [ -f "$SINKHOLE_PID_HTTP" ]; then kill $(cat "$SINKHOLE_PID_HTTP") 2>/dev/null rm -f "$SINKHOLE_PID_HTTP" fi pkill -f "sinkhole-http-handler" 2>/dev/null || true # Stop HTTPS sinkhole if [ -f "$SINKHOLE_PID_HTTPS" ]; then kill $(cat "$SINKHOLE_PID_HTTPS") 2>/dev/null rm -f "$SINKHOLE_PID_HTTPS" fi pkill -f "OPENSSL-LISTEN" 2>/dev/null || true log "Sinkhole server stopped" } sinkhole_status() { echo "" echo -e "${BOLD}Vortex Sinkhole Server${NC}" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" local enabled=$(uci -q get vortex-firewall.server.enabled || echo 0) local http_port=$(uci -q get vortex-firewall.server.http_port || echo 80) local https_port=$(uci -q get vortex-firewall.server.https_port || echo 443) if [ "$enabled" = "1" ]; then echo -e "Config: ${GREEN}Enabled${NC}" else echo -e "Config: ${YELLOW}Disabled${NC}" fi echo "Sinkhole IP: $SINKHOLE_IP" echo "HTTP Port: $http_port" echo "HTTPS Port: $https_port" echo "" # Check running processes if pgrep -f "sinkhole-http-handler" >/dev/null 2>&1; then echo -e "HTTP Server: ${GREEN}Running${NC}" else echo -e "HTTP Server: ${RED}Stopped${NC}" fi # Check HTTPS by PID file (supports multiple backends) if [ -f "$SINKHOLE_PID_HTTPS" ] && kill -0 "$(cat "$SINKHOLE_PID_HTTPS")" 2>/dev/null; then if pgrep -f "OPENSSL-LISTEN\|stunnel\|vortex-stunnel" >/dev/null 2>&1; then echo -e "HTTPS Server: ${GREEN}Running${NC}" else echo -e "HTTPS Server: ${YELLOW}Limited (no SSL)${NC}" fi else echo -e "HTTPS Server: ${RED}Stopped${NC}" fi # Event stats if [ -f "$BLOCKLIST_DB" ]; then local today=$(date +%Y-%m-%d) local total_events=$(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(*) FROM events;" 2>/dev/null || echo 0) local today_events=$(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(*) FROM events WHERE timestamp LIKE '$today%';" 2>/dev/null || echo 0) local unique_clients=$(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(DISTINCT client_ip) FROM events;" 2>/dev/null || echo 0) echo "" echo -e "${BOLD}Capture Statistics:${NC}" echo " Total Events: $total_events" echo " Today's Events: $today_events" echo " Unique Clients: $unique_clients" fi echo "" } sinkhole_logs() { local lines="${1:-50}" echo "" echo -e "${BOLD}Sinkhole Event Log (last $lines)${NC}" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" if [ -f "$BLOCKLIST_DB" ]; then sqlite3 -column -header "$BLOCKLIST_DB" \ "SELECT timestamp, client_ip, domain, event_type FROM events ORDER BY id DESC LIMIT $lines;" 2>/dev/null else warn "No database found" fi echo "" } sinkhole_export() { local output="${1:-/tmp/vortex-sinkhole-events.json}" log "Exporting sinkhole events to $output..." if [ ! -f "$BLOCKLIST_DB" ]; then error "No database found" return 1 fi echo "[" > "$output" local first=1 sqlite3 "$BLOCKLIST_DB" "SELECT id, timestamp, client_ip, domain, event_type, details FROM events ORDER BY id;" 2>/dev/null | \ while IFS='|' read -r id ts ip domain type details; do [ -z "$id" ] && continue [ "$first" = "1" ] && first=0 || echo "," >> "$output" printf '{"id":%d,"timestamp":"%s","client_ip":"%s","domain":"%s","event_type":"%s","details":"%s"}' \ "$id" "$ts" "$ip" "$domain" "$type" "$details" >> "$output" done echo "]" >> "$output" log "Exported to: $output" log "Events: $(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(*) FROM events;" 2>/dev/null || echo 0)" } sinkhole_gencert() { local cert_dir="/etc/vortex-firewall" mkdir -p "$cert_dir" log "Generating self-signed certificate for HTTPS sinkhole..." # Generate private key openssl genrsa -out "$cert_dir/sinkhole.key" 2048 2>/dev/null # Generate self-signed certificate openssl req -new -x509 -key "$cert_dir/sinkhole.key" \ -out "$cert_dir/sinkhole.crt" \ -days 3650 \ -subj "/CN=Vortex Sinkhole/O=SecuBox/C=FR" 2>/dev/null chmod 600 "$cert_dir/sinkhole.key" chmod 644 "$cert_dir/sinkhole.crt" log "Certificate generated:" log " Key: $cert_dir/sinkhole.key" log " Cert: $cert_dir/sinkhole.crt" } sinkhole_clear() { log "Clearing sinkhole event log..." if [ -f "$BLOCKLIST_DB" ]; then sqlite3 "$BLOCKLIST_DB" "DELETE FROM events;" 2>/dev/null log "Events cleared" else warn "No database found" fi } # Record a sinkhole hit (called by sinkhole HTTP servers) sinkhole_record_event() { local client_ip="$1" local domain="$2" local event_type="${3:-http}" local details="${4:-}" [ -z "$client_ip" ] || [ -z "$domain" ] && return 1 init_db local timestamp=$(date -Iseconds) # Record event sqlite3 "$BLOCKLIST_DB" \ "INSERT INTO events (timestamp, client_ip, domain, event_type, details) VALUES ('$timestamp', '$client_ip', '$domain', '$event_type', '$details');" 2>/dev/null # Update hit count on domain sqlite3 "$BLOCKLIST_DB" \ "UPDATE domains SET hit_count = hit_count + 1, last_seen = '$timestamp' WHERE domain = '$domain';" 2>/dev/null # Log to syslog logger -t vortex-sinkhole "Blocked: $client_ip -> $domain ($event_type)" echo "$timestamp" } # ============================================================================ # Mesh Threat Sharing (Phase 4) # ============================================================================ THREAT_INTEL_SCRIPT="/usr/lib/secubox/threat-intel.sh" mesh_status() { echo "" echo -e "${BOLD}Vortex Mesh Threat Sharing${NC}" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" if [ ! -x "$THREAT_INTEL_SCRIPT" ]; then echo -e "Status: ${RED}Not Available${NC}" echo "Install secubox-p2p for mesh threat sharing" return 1 fi # Get threat intel status local status=$("$THREAT_INTEL_SCRIPT" status 2>/dev/null) if [ -z "$status" ]; then echo -e "Status: ${YELLOW}Initializing${NC}" return 0 fi local enabled=$(echo "$status" | jsonfilter -e '@.enabled' 2>/dev/null) local local_iocs=$(echo "$status" | jsonfilter -e '@.local_iocs' 2>/dev/null || echo 0) local received=$(echo "$status" | jsonfilter -e '@.received_iocs' 2>/dev/null || echo 0) local applied=$(echo "$status" | jsonfilter -e '@.applied_iocs' 2>/dev/null || echo 0) local peers=$(echo "$status" | jsonfilter -e '@.peer_contributors' 2>/dev/null || echo 0) local chain_blocks=$(echo "$status" | jsonfilter -e '@.chain_threat_blocks' 2>/dev/null || echo 0) if [ "$enabled" = "true" ]; then echo -e "Status: ${GREEN}Enabled${NC}" else echo -e "Status: ${YELLOW}Disabled${NC}" fi echo "" echo -e "${BOLD}Threat Intelligence:${NC}" echo " Local IOCs: $local_iocs (from this node)" echo " Received IOCs: $received (from mesh)" echo " Applied IOCs: $applied" echo " Peer Contributors: $peers" echo " Chain Blocks: $chain_blocks" # Count Vortex-sourced IOCs in local local vortex_local=0 local ti_local="/var/lib/secubox/threat-intel/iocs-local.json" if [ -f "$ti_local" ]; then vortex_local=$(jsonfilter -i "$ti_local" -e '@[*].source' 2>/dev/null | grep -c "^vortex$" || echo 0) fi echo "" echo -e "${BOLD}Vortex Contributions:${NC}" echo " Domains Shared: $vortex_local" echo "" } mesh_publish() { log "Publishing Vortex domains to mesh..." if [ ! -x "$THREAT_INTEL_SCRIPT" ]; then error "secubox-p2p not installed" return 1 fi # Collect and publish "$THREAT_INTEL_SCRIPT" collect 2>/dev/null local result=$("$THREAT_INTEL_SCRIPT" publish 2>/dev/null) local published=$(echo "$result" | jsonfilter -e '@.published' 2>/dev/null || echo 0) log "Published $published IOCs to mesh" } mesh_sync() { log "Syncing threats from mesh..." if [ ! -x "$THREAT_INTEL_SCRIPT" ]; then error "secubox-p2p not installed" return 1 fi # Process pending blocks and apply local result=$("$THREAT_INTEL_SCRIPT" apply-pending 2>/dev/null) local applied=$(echo "$result" | jsonfilter -e '@.applied' 2>/dev/null || echo 0) local skipped=$(echo "$result" | jsonfilter -e '@.skipped' 2>/dev/null || echo 0) log "Applied: $applied, Skipped: $skipped" # Regenerate blocklist with new domains if [ "$applied" -gt 0 ]; then generate_blocklist fi } mesh_received() { local lines="${1:-20}" echo "" echo -e "${BOLD}Received Threats from Mesh${NC}" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" if [ ! -x "$THREAT_INTEL_SCRIPT" ]; then warn "secubox-p2p not installed" return 1 fi local received=$("$THREAT_INTEL_SCRIPT" list received 2>/dev/null) local count=$(echo "$received" | jsonfilter -e '@[*]' 2>/dev/null | wc -l) if [ "$count" -eq 0 ]; then info "No threats received from mesh yet" return 0 fi echo "Total: $count received IOCs" echo "" # Show recent domain IOCs echo "$received" | jsonfilter -e '@[*]' 2>/dev/null | tail -n "$lines" | while read -r ioc; do local domain=$(echo "$ioc" | jsonfilter -e '@.domain' 2>/dev/null) local ip=$(echo "$ioc" | jsonfilter -e '@.ip' 2>/dev/null) local severity=$(echo "$ioc" | jsonfilter -e '@.severity' 2>/dev/null) local trust=$(echo "$ioc" | jsonfilter -e '@.trust' 2>/dev/null) local applied=$(echo "$ioc" | jsonfilter -e '@.applied' 2>/dev/null) local scenario=$(echo "$ioc" | jsonfilter -e '@.scenario' 2>/dev/null) local target="${domain:-$ip}" [ -z "$target" ] && continue local status_icon="\u2705" [ "$applied" = "false" ] && status_icon="\u23F3" printf "%-35s " "$target" printf "%-10s " "$severity" printf "%-12s " "$trust" printf "%-20s " "$scenario" echo -e "$status_icon" done echo "" } mesh_peers() { echo "" echo -e "${BOLD}Mesh Peer Contributions${NC}" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" if [ ! -x "$THREAT_INTEL_SCRIPT" ]; then warn "secubox-p2p not installed" return 1 fi local peers=$("$THREAT_INTEL_SCRIPT" peers 2>/dev/null) local count=$(echo "$peers" | jsonfilter -e '@[*]' 2>/dev/null | wc -l) if [ "$count" -eq 0 ]; then info "No peer contributions yet" return 0 fi echo "$peers" | jsonfilter -e '@[*]' 2>/dev/null | while read -r peer; do local node=$(echo "$peer" | jsonfilter -e '@.node' 2>/dev/null) local trust=$(echo "$peer" | jsonfilter -e '@.trust' 2>/dev/null) local ioc_count=$(echo "$peer" | jsonfilter -e '@.ioc_count' 2>/dev/null) local applied_count=$(echo "$peer" | jsonfilter -e '@.applied_count' 2>/dev/null) printf "%-20s " "${node:0:20}" printf "%-12s " "$trust" printf "IOCs: %-5s " "$ioc_count" printf "Applied: %-5s\n" "$applied_count" done echo "" } # ============================================================================ # DNS Guard Integration (Phase 3) # ============================================================================ DNSGUARD_DIR="/var/lib/dns-guard" DNSGUARD_BLOCKLIST_DIR="/etc/dns-guard/blocklists" dnsguard_status() { echo "" echo -e "${BOLD}DNS Guard Integration${NC}" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" # Check DNS Guard service if pgrep -f "dns-guard" >/dev/null 2>&1; then echo -e "Service: ${GREEN}Running${NC}" elif [ -x /etc/init.d/dns-guard ]; then local enabled=$(/etc/init.d/dns-guard enabled && echo yes || echo no) if [ "$enabled" = "yes" ]; then echo -e "Service: ${YELLOW}Enabled (not running)${NC}" else echo -e "Service: ${RED}Disabled${NC}" fi else echo -e "Service: ${RED}Not installed${NC}" return 1 fi # Data files echo "" echo -e "${BOLD}Data Files:${NC}" if [ -f "$DNSGUARD_DIR/alerts.json" ]; then local alert_count=$(jsonfilter -i "$DNSGUARD_DIR/alerts.json" -e '@[*]' 2>/dev/null | wc -l) echo " Alerts: $alert_count entries" else echo " Alerts: (no file)" fi if [ -f "$DNSGUARD_DIR/threat_domains.txt" ]; then local domain_count=$(wc -l < "$DNSGUARD_DIR/threat_domains.txt" 2>/dev/null || echo 0) echo " Threats: $domain_count domains" else echo " Threats: (no file)" fi if [ -f "$DNSGUARD_DIR/pending_blocks.json" ]; then local pending_count=$(jsonfilter -i "$DNSGUARD_DIR/pending_blocks.json" -e '@[*]' 2>/dev/null | wc -l) echo " Pending: $pending_count approvals" else echo " Pending: (no file)" fi # Vortex import stats echo "" echo -e "${BOLD}Vortex Integration:${NC}" if [ -f "$BLOCKLIST_DB" ]; then local imported=$(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(*) FROM domains WHERE source='dnsguard';" 2>/dev/null || echo 0) local last_update=$(sqlite3 "$BLOCKLIST_DB" "SELECT last_update FROM feeds WHERE name='dnsguard';" 2>/dev/null || echo "never") echo " Imported: $imported domains" echo " Last Sync: $last_update" # Threat type breakdown echo "" echo -e "${BOLD}Detection Types from DNS Guard:${NC}" sqlite3 "$BLOCKLIST_DB" \ "SELECT ' ' || threat_type || ': ' || COUNT(*) FROM domains WHERE source='dnsguard' GROUP BY threat_type;" 2>/dev/null fi echo "" } dnsguard_sync() { log "Syncing with DNS Guard..." feed_import_dnsguard # Regenerate blocklist with new entries generate_blocklist log "DNS Guard sync complete" } dnsguard_export() { # Export Vortex threat intel back to DNS Guard blocklists (bidirectional) log "Exporting Vortex intel to DNS Guard blocklists..." mkdir -p "$DNSGUARD_BLOCKLIST_DIR" local export_file="$DNSGUARD_BLOCKLIST_DIR/vortex-firewall.txt" # Export domains from external feeds (not DNS Guard's own detections) sqlite3 "$BLOCKLIST_DB" \ "SELECT domain FROM domains WHERE blocked=1 AND source != 'dnsguard';" 2>/dev/null > "$export_file" local count=$(wc -l < "$export_file" 2>/dev/null || echo 0) log "Exported $count domains to: $export_file" # Signal DNS Guard to reload if running if pgrep -f "dns-guard" >/dev/null 2>&1; then killall -HUP dns-guard 2>/dev/null || true log "Signaled DNS Guard to reload" fi } dnsguard_alerts() { local lines="${1:-20}" echo "" echo -e "${BOLD}Recent DNS Guard Alerts${NC}" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" if [ ! -f "$DNSGUARD_DIR/alerts.json" ]; then warn "No alerts file found" return 1 fi # Parse and display recent alerts jsonfilter -i "$DNSGUARD_DIR/alerts.json" -e '@[*]' 2>/dev/null | tail -n "$lines" | while read -r alert; do local domain=$(echo "$alert" | jsonfilter -e '@.domain' 2>/dev/null) local client=$(echo "$alert" | jsonfilter -e '@.client' 2>/dev/null) local type=$(echo "$alert" | jsonfilter -e '@.type' 2>/dev/null) local confidence=$(echo "$alert" | jsonfilter -e '@.confidence' 2>/dev/null) local reason=$(echo "$alert" | jsonfilter -e '@.reason' 2>/dev/null) [ -z "$domain" ] && continue printf "${YELLOW}%-30s${NC} " "$domain" printf "%-12s " "$type" printf "${CYAN}%3s%%${NC} " "$confidence" printf "client=%s" "$client" echo "" done echo "" } # ============================================================================ # Service Control # ============================================================================ service_start() { log "Starting Vortex DNS Firewall..." init_dirs init_db # Initial feed update if no blocklist exists if [ ! -f "$BLOCKLIST_HOSTS" ] || [ $(wc -l < "$BLOCKLIST_HOSTS" 2>/dev/null || echo 0) -lt 10 ]; then intel_update fi # Start sinkhole if enabled local sinkhole_enabled=$(uci -q get vortex-firewall.server.enabled) [ "$sinkhole_enabled" = "1" ] && sinkhole_start log "Vortex DNS Firewall active" log "Sinkhole IP: $SINKHOLE_IP" log "Blocked domains: $(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(*) FROM domains WHERE blocked=1;")" } service_stop() { log "Stopping Vortex DNS Firewall..." # Stop sinkhole server sinkhole_stop # Remove dnsmasq config rm -f "$DNSMASQ_CONF" # Reload dnsmasq /etc/init.d/dnsmasq restart 2>/dev/null log "Vortex DNS Firewall stopped" } service_status() { echo "" echo -e "${BOLD}Vortex DNS Firewall v$VERSION${NC}" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" if [ -f "$DNSMASQ_CONF" ]; then echo -e "Status: ${GREEN}Active${NC}" else echo -e "Status: ${RED}Inactive${NC}" fi echo "Sinkhole IP: $SINKHOLE_IP" if [ -f "$BLOCKLIST_DB" ]; then local count=$(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(*) FROM domains WHERE blocked=1;" 2>/dev/null || echo 0) echo "Blocked: $count domains" else echo "Blocked: (no database)" fi if [ -f "$STATS_FILE" ]; then local last=$(jsonfilter -i "$STATS_FILE" -e '@.last_update' 2>/dev/null || echo "never") echo "Last Update: $last" fi echo "" } # ============================================================================ # Usage # ============================================================================ usage() { cat <<'EOF' Vortex DNS Firewall - Block threats at DNS level Usage: vortex-firewall [options] Intel Commands: intel update Update all threat feeds intel status Show feed status and stats intel search Check if domain is blocked intel add Manually block a domain intel remove Unblock a domain DNS Guard Integration (Phase 3): dnsguard status Show DNS Guard integration status dnsguard sync Force sync detections from DNS Guard dnsguard export Export Vortex intel to DNS Guard blocklists dnsguard alerts [N] Show recent DNS Guard alerts (default: 20) Mesh Threat Sharing (Phase 4): mesh status Show mesh threat sharing status mesh publish Publish local domains to mesh mesh sync Sync and apply threats from mesh mesh received [N] Show threats received from mesh (default: 20) mesh peers Show peer contribution statistics Sinkhole Server: sinkhole start Start HTTP/HTTPS sinkhole server sinkhole stop Stop sinkhole server sinkhole status Show sinkhole status and stats sinkhole logs [N] Show last N sinkhole events (default: 50) sinkhole export [file] Export events to JSON file sinkhole gencert Generate self-signed HTTPS certificate sinkhole clear Clear event log Statistics: stats Show blocking statistics stats --x47 Show ×47 impact score stats --top-blocked Top blocked domains Service: start Start firewall (includes sinkhole if enabled) stop Stop firewall status Show service status The ×47 multiplier: Each DNS block prevents ~47 malicious connections (based on typical C2 beacon rate × average infection detection window) Examples: vortex-firewall intel update vortex-firewall intel search evil.com vortex-firewall intel add malware.example.com c2 vortex-firewall dnsguard status vortex-firewall dnsguard sync vortex-firewall sinkhole start vortex-firewall sinkhole logs 100 vortex-firewall stats --x47 EOF } # ============================================================================ # Main # ============================================================================ case "${1:-}" in intel) shift case "${1:-}" in update) intel_update ;; status) intel_status ;; search) shift; intel_search "$@" ;; add) shift; intel_add "$@" ;; remove) shift; intel_remove "$@" ;; *) error "Unknown intel command. Use: update, status, search, add, remove" ;; esac ;; sinkhole) shift case "${1:-}" in start) sinkhole_start ;; stop) sinkhole_stop ;; status) sinkhole_status ;; logs) shift; sinkhole_logs "$@" ;; export) shift; sinkhole_export "$@" ;; gencert) sinkhole_gencert ;; clear) sinkhole_clear ;; record) shift; sinkhole_record_event "$@" ;; *) error "Unknown sinkhole command. Use: start, stop, status, logs, export, gencert, clear" ;; esac ;; stats) shift case "${1:-}" in --x47|-x) show_x47 ;; --top*) show_stats ;; *) show_stats ;; esac ;; dnsguard) shift case "${1:-}" in status) dnsguard_status ;; sync) dnsguard_sync ;; export) dnsguard_export ;; alerts) shift; dnsguard_alerts "$@" ;; *) error "Unknown dnsguard command. Use: status, sync, export, alerts" ;; esac ;; mesh) shift case "${1:-}" in status) mesh_status ;; publish) mesh_publish ;; sync) mesh_sync ;; received) shift; mesh_received "$@" ;; peers) mesh_peers ;; *) error "Unknown mesh command. Use: status, publish, sync, received, peers" ;; esac ;; start) service_start ;; stop) service_stop ;; status) service_status ;; help|--help|-h) usage ;; "") service_status ;; *) error "Unknown command: $1" usage >&2 exit 1 ;; esac