secubox-openwrt/package/secubox/secubox-vortex-firewall/root/usr/sbin/vortex-firewall
CyberMind-FR d2953c5807 feat(vortex-firewall): Add DNS-level threat blocking with x47 multiplier
Phase 1 implementation of Vortex DNS Firewall - SecuBox's first line
of defense blocking threats at DNS level BEFORE any connection is
established.

Features:
- Threat intel aggregator (URLhaus, OpenPhish, Malware Domains)
- SQLite-based blocklist database with domain deduplication
- dnsmasq integration via sinkhole hosts file
- x47 vitality multiplier concept (each DNS block prevents ~47 connections)
- RPCD handler for LuCI integration with 8 methods
- CLI tool: vortex-firewall intel/stats/start/stop

Tested with 765 blocked domains across 3 threat feeds.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-11 06:58:02 +01:00

571 lines
18 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 <command> Threat intelligence management
# vortex-firewall stats Show blocking statistics
# vortex-firewall sinkhole <command> Sinkhole server management
# vortex-firewall mesh <command> 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_MALWAREDOMAINS="https://mirror1.malwaredomains.com/files/justdomains"
# 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" <<EOF
CREATE TABLE IF NOT EXISTS domains (
domain TEXT PRIMARY KEY,
threat_type TEXT,
confidence INTEGER DEFAULT 80,
source TEXT,
first_seen TEXT,
last_seen TEXT,
hit_count INTEGER DEFAULT 0,
blocked INTEGER DEFAULT 1
);
CREATE TABLE IF NOT EXISTS feeds (
name TEXT PRIMARY KEY,
url TEXT,
last_update TEXT,
domain_count INTEGER DEFAULT 0,
enabled INTEGER DEFAULT 1
);
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT,
domain TEXT,
client_ip TEXT,
event_type TEXT,
details TEXT
);
CREATE INDEX IF NOT EXISTS idx_domain ON domains(domain);
CREATE INDEX IF NOT EXISTS idx_threat ON domains(threat_type);
CREATE INDEX IF NOT EXISTS idx_events_ts ON events(timestamp);
EOF
log "Database initialized: $BLOCKLIST_DB"
fi
}
# ============================================================================
# Feed Management
# ============================================================================
feed_update_urlhaus() {
local feed_file="$FEEDS_DIR/urlhaus.txt"
log "Updating URLhaus feed..."
if curl -sL --connect-timeout 10 --max-time 60 "$FEED_URLHAUS" -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 ('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_malwaredomains() {
local feed_file="$FEEDS_DIR/malwaredomains.txt"
log "Updating Malware Domains feed..."
if curl -sL --connect-timeout 10 --max-time 60 "$FEED_MALWAREDOMAINS" -o "$feed_file.tmp" 2>/dev/null; then
grep -v '^#' "$feed_file.tmp" 2>/dev/null | 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 ('malwaredomains', '$FEED_MALWAREDOMAINS', datetime('now'), $count, 1);"
log "Malware Domains: $count domains"
return 0
else
warn "Failed to update Malware Domains 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_malwaredomains && 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
}
intel_merge() {
log "Merging feeds into blocklist..."
local now=$(date -Iseconds)
# 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) threat_type="malware" ;;
dnsguard) threat_type="ai_detected" ;;
feodo) threat_type="c2" ;;
esac
while read -r domain; do
[ -z "$domain" ] && continue
[ "${domain:0:1}" = "#" ] && continue
# Clean domain
domain=$(echo "$domain" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9.-]//g')
[ -z "$domain" ] && continue
sqlite3 "$BLOCKLIST_DB" "INSERT OR IGNORE INTO domains (domain, threat_type, source, first_seen, last_seen)
VALUES ('$domain', '$threat_type', '$feed_name', '$now', '$now');"
sqlite3 "$BLOCKLIST_DB" "UPDATE domains SET last_seen='$now', source='$feed_name' WHERE domain='$domain';"
done < "$feed_file"
done
local total=$(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(*) FROM domains WHERE blocked=1;")
log "Total blocked domains: $total"
}
generate_blocklist() {
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"
local count=$(grep -c "^$SINKHOLE_IP" "$BLOCKLIST_HOSTS")
log "Generated $count sinkhole entries"
# Generate dnsmasq config
cat > "$DNSMASQ_CONF" <<EOF
# Vortex DNS Firewall Configuration
# Generated: $(date)
# Block count: $count
# Load sinkhole hosts
addn-hosts=$BLOCKLIST_HOSTS
# Log queries for analysis
log-queries
log-facility=/var/log/dnsmasq.log
EOF
log "dnsmasq config written: $DNSMASQ_CONF"
# Reload dnsmasq
if [ -x /etc/init.d/dnsmasq ]; then
/etc/init.d/dnsmasq restart 2>/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 <domain>"; 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 <domain> [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 <domain>"; 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 <command> [options]
Intel Commands:
intel update Update all threat feeds
intel status Show feed status and stats
intel search <domain> Check if domain is blocked
intel add <domain> Manually block a domain
intel remove <domain> 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