#!/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_list="/var/lib/dns-guard/threat_domains.txt" local feed_file="$FEEDS_DIR/dnsguard.txt" if [ -f "$dnsguard_list" ]; then log "Importing DNS Guard detections..." 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" return 0 else info "No DNS Guard detections found" return 0 fi } 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 "" } # ============================================================================ # 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 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..." # 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 Statistics: stats Show blocking statistics stats --x47 Show ×47 impact score stats --top-blocked Top blocked domains Service: start Start firewall 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 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 ;; stats) shift case "${1:-}" in --x47|-x) show_x47 ;; --top*) show_stats ;; *) show_stats ;; 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