#!/bin/bash # XDP Defense - Unified CLI for XDP Blocker + DDoS Defense # Combines CIDR/country/whitelist blocking with rate limiting + AI detection # Uses libxdp dispatcher for chaining two XDP programs PROJ_DIR="/opt/xdp-defense" BPF_DIR="$PROJ_DIR/bpf" LIB_DIR="$PROJ_DIR/lib" PIN_PATH="/sys/fs/bpf/xdp-defense" CONFIG_FILE="/etc/xdp-defense/config.yaml" DATA_DIR="/var/lib/xdp-defense" PID_FILE="$DATA_DIR/daemon.pid" BLOCKLIST_FILE="/etc/xdp-defense/blocklist.txt" COUNTRY_DIR="/etc/xdp-defense/countries" GEOIP_DB="/usr/share/GeoIP/GeoLite2-Country.mmdb" CITY_DB="/usr/share/GeoIP/GeoLite2-City.mmdb" ASN_DB="/usr/share/GeoIP/GeoLite2-ASN.mmdb" # Ensure Python can find xdp_common.py (installed to /usr/local/bin) export PYTHONPATH="/usr/local/bin:${PYTHONPATH:-}" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' log_ok() { echo -e "${GREEN}[OK]${NC} $1"; logger -t xdp-defense "OK: $1" 2>/dev/null || true; } log_err() { echo -e "${RED}[ERROR]${NC} $1" >&2; logger -t xdp-defense -p user.err "ERROR: $1" 2>/dev/null || true; } log_info() { echo -e "${CYAN}[INFO]${NC} $1"; logger -t xdp-defense "INFO: $1" 2>/dev/null || true; } # Sanitize input - strict allowlist for IP addresses, CIDR notation, preset names sanitize_input() { local val="$1" # Allow only alphanumeric, dots, colons, slashes, hyphens, underscores if [[ ! "$val" =~ ^[a-zA-Z0-9._:/\ -]+$ ]]; then log_err "Invalid characters in input: $val" return 1 fi echo "$val" } get_iface() { if [ -f "$CONFIG_FILE" ]; then local iface iface=$(python3 -c " import yaml, sys with open(sys.argv[1]) as f: print(yaml.safe_load(f).get('general',{}).get('interface','eth0')) " "$CONFIG_FILE" 2>/dev/null) echo "${iface:-eth0}" else echo "eth0" fi } get_map_id() { bpftool map show -j 2>/dev/null | python3 -c " import sys, json try: maps = json.load(sys.stdin) for m in maps: if m.get('name') == '$1': print(m['id']) break except: pass " 2>/dev/null } validate_cidr() { local input=$1 local ip prefix if [[ "$input" == *"/"* ]]; then ip="${input%/*}" prefix="${input#*/}" else ip="$input" prefix=32 fi if ! [[ "$prefix" =~ ^[0-9]+$ ]] || [ "$prefix" -lt 0 ] || [ "$prefix" -gt 32 ]; then log_err "Invalid prefix: /$prefix (must be 0-32)" return 1 fi IFS='.' read -r a b c d <<< "$ip" if [ -z "$a" ] || [ -z "$b" ] || [ -z "$c" ] || [ -z "$d" ]; then log_err "Invalid IP format: $ip" return 1 fi for octet in "$a" "$b" "$c" "$d"; do if ! [[ "$octet" =~ ^[0-9]+$ ]] || [ "$octet" -lt 0 ] || [ "$octet" -gt 255 ]; then log_err "Invalid IP octet: $octet (must be 0-255)" return 1 fi done return 0 } # ==================== Load / Unload (xdp-loader) ==================== cmd_load() { local iface=${1:-$(get_iface)} [ ! -f "$BPF_DIR/xdp_blocker.o" ] || [ ! -f "$BPF_DIR/xdp_ddos.o" ] && cmd_build # Unload any existing XDP programs xdp-loader unload "$iface" --all 2>/dev/null || true # Create pin path mkdir -p "$PIN_PATH" log_info "Loading XDP blocker (priority 10) on $iface..." xdp-loader load "$iface" "$BPF_DIR/xdp_blocker.o" \ --pin-path "$PIN_PATH" log_info "Loading XDP DDoS (priority 20) on $iface..." xdp-loader load "$iface" "$BPF_DIR/xdp_ddos.o" \ --pin-path "$PIN_PATH" # Enable blocker filtering cmd_blocker_enable quiet # Apply rate config cmd_ddos_config_apply quiet log_ok "XDP Defense loaded on $iface (2 programs chained)" # Restore blocklist [ -f "$BLOCKLIST_FILE" ] && cmd_blocker_restore || true # Restore blocked countries if [ -d "$COUNTRY_DIR" ]; then for cc_file in "$COUNTRY_DIR"/*.txt; do [ -f "$cc_file" ] || continue local cc cc=$(basename "$cc_file" .txt) log_info "Restoring country: $cc" python3 "$LIB_DIR/xdp_country.py" add "$cc" >/dev/null 2>&1 || true done fi # Restore whitelists if [ -d "/etc/xdp-defense/whitelist" ]; then for wl_file in /etc/xdp-defense/whitelist/*.txt; do [ -f "$wl_file" ] || continue local name name=$(basename "$wl_file" .txt) log_info "Restoring whitelist: $name" python3 "$LIB_DIR/xdp_whitelist.py" add "$name" >/dev/null 2>&1 || true done fi } cmd_unload() { local iface=${1:-$(get_iface)} xdp-loader unload "$iface" --all 2>/dev/null || true log_ok "XDP Defense unloaded from $iface" } cmd_build() { echo "Building XDP programs..." make -C "$PROJ_DIR" build log_ok "Built: xdp_blocker.o + xdp_ddos.o" } cmd_status() { echo -e "${BOLD}=== XDP Defense Status ===${NC}" echo "" # Check xdp-loader status local iface iface=$(get_iface) local loader_status loader_status=$(xdp-loader status "$iface" 2>/dev/null || true) if echo "$loader_status" | grep -q "xdp_blocker\|xdp_ddos"; then echo -e "XDP Programs: ${GREEN}LOADED${NC}" echo "$loader_status" | grep -E "prog_id|prio" | head -10 | while read -r line; do echo " $line" done else # Fallback: check bpftool local blocker_prog blocker_prog=$(bpftool prog show 2>/dev/null | grep "xdp_blocker") local ddos_prog ddos_prog=$(bpftool prog show 2>/dev/null | grep "xdp_ddos") if [ -n "$blocker_prog" ] && [ -n "$ddos_prog" ]; then echo -e "XDP Programs: ${GREEN}LOADED (both)${NC}" elif [ -n "$blocker_prog" ]; then echo -e "XDP Programs: ${YELLOW}PARTIAL (blocker only)${NC}" elif [ -n "$ddos_prog" ]; then echo -e "XDP Programs: ${YELLOW}PARTIAL (ddos only)${NC}" else echo -e "XDP Programs: ${RED}NOT LOADED${NC}" fi fi echo -e "Interface: ${GREEN}${iface}${NC}" # Blocker status local cfg_id cfg_id=$(get_map_id config) if [ -n "$cfg_id" ]; then local val val=$(bpftool map lookup id "$cfg_id" key 0 0 0 0 2>/dev/null | grep -oP '"value":\s*1' | head -1) [ -n "$val" ] && echo -e "Blocker: ${GREEN}ENABLED${NC}" || echo -e "Blocker: ${YELLOW}DISABLED${NC}" fi # Map counts local bl_id wl_id bl_id=$(get_map_id blocklist_v4) wl_id=$(get_map_id whitelist_v4) if [ -n "$wl_id" ]; then local wl_count wl_count=$(bpftool map dump id "$wl_id" -j 2>/dev/null | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo 0) echo "Whitelist: $wl_count entries" fi if [ -n "$bl_id" ]; then local bl_count bl_count=$(bpftool map dump id "$bl_id" -j 2>/dev/null | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo 0) echo "Blocklist: $bl_count entries" fi # Blocked countries if [ -d "$COUNTRY_DIR" ] && [ "$(ls -A "$COUNTRY_DIR" 2>/dev/null)" ]; then echo "" echo "Countries:" for cc_file in "$COUNTRY_DIR"/*.txt; do [ -f "$cc_file" ] || continue local cc cc=$(basename "$cc_file" .txt | tr 'a-z' 'A-Z') local count count=$(wc -l < "$cc_file") echo " $cc: $count CIDRs" done fi # Daemon echo "" if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then echo -e "Daemon: ${GREEN}RUNNING${NC} (PID $(cat "$PID_FILE"))" else echo -e "Daemon: ${RED}STOPPED${NC}" fi # Rate config python3 -c " from xdp_common import read_rate_config cfg = read_rate_config() if cfg: print(f'Rate limit: {cfg[\"pps_threshold\"]} pps (window: {cfg[\"window_ns\"] // 1_000_000_000}s)') else: print('Rate limit: (not configured)') " 2>/dev/null || true # Blocked IPs python3 -c " from xdp_common import dump_blocked_ips v4 = dump_blocked_ips('blocked_ips_v4') v6 = dump_blocked_ips('blocked_ips_v6') print(f'Blocked IPs: {len(v4) + len(v6)}') " 2>/dev/null || true # AI status echo "" cmd_ai_status 2>/dev/null || true } # ==================== Blocker Commands ==================== cmd_blocker_add() { local cidr cidr=$(sanitize_input "$1") || exit 1 [ -z "$cidr" ] && { log_err "Usage: xdp-defense blocker add "; exit 1; } if [[ "$cidr" == *":"* ]]; then local map_id map_id=$(get_map_id blocklist_v6) [ -z "$map_id" ] && { log_err "IPv6 map not found. Is XDP loaded?"; exit 1; } local key_hex key_hex=$(python3 -c "import sys; from xdp_common import cidr_to_key_v6; print(cidr_to_key_v6(sys.argv[1]))" "$cidr" 2>/dev/null) [ -z "$key_hex" ] && { log_err "Invalid IPv6 CIDR: $cidr"; exit 1; } [[ "$key_hex" =~ ^[0-9a-f\ ]+$ ]] || { log_err "Invalid key hex"; exit 1; } bpftool map update id "$map_id" key hex $key_hex value hex 01 00 00 00 00 00 00 00 2>/dev/null [ "$2" != "quiet" ] && log_ok "Added (v6): $cidr" || true mkdir -p "$(dirname "$BLOCKLIST_FILE")" grep -qxF "$cidr" "$BLOCKLIST_FILE" 2>/dev/null || echo "$cidr" >> "$BLOCKLIST_FILE" return fi validate_cidr "$cidr" || exit 1 local ip prefix if [[ "$cidr" == *"/"* ]]; then ip="${cidr%/*}" prefix="${cidr#*/}" else ip="$cidr" prefix=32 fi local map_id map_id=$(get_map_id blocklist_v4) [ -z "$map_id" ] && { log_err "Map not found. Is XDP loaded?"; exit 1; } IFS='.' read -r a b c d <<< "$ip" local key_hex key_hex=$(printf '%02x 00 00 00 %02x %02x %02x %02x' "$prefix" "$a" "$b" "$c" "$d") [[ "$key_hex" =~ ^[0-9a-f\ ]+$ ]] || { log_err "Invalid key hex"; exit 1; } bpftool map update id "$map_id" key hex $key_hex value hex 01 00 00 00 00 00 00 00 2>/dev/null [ "$2" != "quiet" ] && log_ok "Added: $cidr" || true mkdir -p "$(dirname "$BLOCKLIST_FILE")" grep -qxF "$cidr" "$BLOCKLIST_FILE" 2>/dev/null || echo "$cidr" >> "$BLOCKLIST_FILE" } cmd_blocker_del() { local cidr cidr=$(sanitize_input "$1") || exit 1 [ -z "$cidr" ] && { log_err "Usage: xdp-defense blocker del "; exit 1; } if [[ "$cidr" == *":"* ]]; then local map_id map_id=$(get_map_id blocklist_v6) [ -z "$map_id" ] && { log_err "IPv6 map not found"; exit 1; } local key_hex key_hex=$(python3 -c "import sys; from xdp_common import cidr_to_key_v6; print(cidr_to_key_v6(sys.argv[1]))" "$cidr" 2>/dev/null) [ -z "$key_hex" ] && { log_err "Invalid IPv6 CIDR: $cidr"; exit 1; } [[ "$key_hex" =~ ^[0-9a-f\ ]+$ ]] || { log_err "Invalid key hex"; exit 1; } bpftool map delete id "$map_id" key hex $key_hex 2>/dev/null && log_ok "Removed (v6): $cidr" local tmpfile="${BLOCKLIST_FILE}.tmp.$$" grep -vxF "$cidr" "$BLOCKLIST_FILE" > "$tmpfile" 2>/dev/null && mv "$tmpfile" "$BLOCKLIST_FILE" || rm -f "$tmpfile" return fi validate_cidr "$cidr" || exit 1 local ip prefix if [[ "$cidr" == *"/"* ]]; then ip="${cidr%/*}" prefix="${cidr#*/}" else ip="$cidr" prefix=32 fi local map_id map_id=$(get_map_id blocklist_v4) [ -z "$map_id" ] && { log_err "Map not found"; exit 1; } IFS='.' read -r a b c d <<< "$ip" local key_hex key_hex=$(printf '%02x 00 00 00 %02x %02x %02x %02x' "$prefix" "$a" "$b" "$c" "$d") [[ "$key_hex" =~ ^[0-9a-f\ ]+$ ]] || { log_err "Invalid key hex"; exit 1; } bpftool map delete id "$map_id" key hex $key_hex 2>/dev/null && log_ok "Removed: $cidr" local tmpfile="${BLOCKLIST_FILE}.tmp.$$" grep -vxF "$cidr" "$BLOCKLIST_FILE" > "$tmpfile" 2>/dev/null && mv "$tmpfile" "$BLOCKLIST_FILE" || rm -f "$tmpfile" } cmd_blocker_list() { local map_id map_id=$(get_map_id blocklist_v4) [ -z "$map_id" ] && { log_err "Map not found"; exit 1; } echo "=== Blocked CIDRs ===" bpftool map dump id "$map_id" -j 2>/dev/null | \ python3 -c " import sys, json, socket, struct data = json.load(sys.stdin) for entry in data: fmt = entry.get('formatted', entry) key = fmt['key'] prefix = key['prefixlen'] addr = key['addr'] ip = socket.inet_ntoa(struct.pack('/dev/null || echo " (empty)" } cmd_blocker_stats() { local map_id map_id=$(get_map_id stats_map) [ -z "$map_id" ] && { log_err "Stats map not found"; exit 1; } echo "=== Blocker Statistics ===" bpftool map dump id "$map_id" -j 2>/dev/null | \ python3 -c " import sys, json data = json.load(sys.stdin) labels = ['Passed', 'Dropped', 'Whitelist', 'Errors'] for entry in data: fmt = entry.get('formatted', entry) key = fmt['key'] if isinstance(fmt['key'], int) else int(fmt['key'][0], 16) values = fmt.get('values', []) total_pkts = sum(v['value']['packets'] for v in values) total_bytes = sum(v['value']['bytes'] for v in values) if key < len(labels): print(f'{labels[key]:9}: {total_pkts:>10} pkts, {total_bytes:>12} bytes') " } cmd_blocker_enable() { local map_id map_id=$(get_map_id config) [ -n "$map_id" ] && bpftool map update id "$map_id" key 0 0 0 0 value 1 0 0 0 [ "$1" != "quiet" ] && log_ok "Blocker filtering enabled" || true } cmd_blocker_disable() { local map_id map_id=$(get_map_id config) [ -n "$map_id" ] && bpftool map update id "$map_id" key 0 0 0 0 value 0 0 0 0 log_ok "Blocker filtering disabled" } cmd_blocker_restore() { [ ! -f "$BLOCKLIST_FILE" ] && return local count=0 while read -r cidr; do [[ -z "$cidr" || "$cidr" == "#"* ]] && continue cmd_blocker_add "$cidr" quiet 2>/dev/null && ((count++)) || true done < "$BLOCKLIST_FILE" log_ok "Restored $count blocklist entries" } # ==================== Country Commands ==================== cmd_country_add() { local cc=$(echo "$1" | tr 'A-Z' 'a-z') [ -z "$cc" ] && { log_err "Usage: xdp-defense country add "; exit 1; } [[ "$cc" =~ ^[a-z]{2}$ ]] || { log_err "Invalid country code: $cc (must be 2 letters)"; exit 1; } python3 "$LIB_DIR/xdp_country.py" add "$cc" } cmd_country_del() { local cc=$(echo "$1" | tr 'A-Z' 'a-z') [ -z "$cc" ] && { log_err "Usage: xdp-defense country del "; exit 1; } python3 "$LIB_DIR/xdp_country.py" del "$cc" } cmd_country_list() { python3 "$LIB_DIR/xdp_country.py" list } # ==================== Whitelist Commands ==================== cmd_whitelist_add() { local name name=$(sanitize_input "$1") || exit 1 [ -z "$name" ] && { log_err "Usage: xdp-defense whitelist add "; exit 1; } # Direct IP/CIDR: contains a dot (IPv4) or colon (IPv6) with digits if [[ "$name" =~ ^[0-9]+\.[0-9]+ ]] || [[ "$name" =~ ^[0-9a-fA-F]*: ]]; then local map_name key_hex if [[ "$name" == *":"* ]]; then map_name="whitelist_v6" key_hex=$(python3 -c "import sys; from xdp_common import cidr_to_key_v6; print(cidr_to_key_v6(sys.argv[1]))" "$name" 2>/dev/null) else map_name="whitelist_v4" key_hex=$(python3 -c "import sys; from xdp_common import cidr_to_key; print(cidr_to_key(sys.argv[1]))" "$name" 2>/dev/null) fi [ -z "$key_hex" ] && { log_err "Invalid CIDR: $name"; exit 1; } [[ "$key_hex" =~ ^[0-9a-f\ ]+$ ]] || { log_err "Invalid key hex"; exit 1; } local map_id map_id=$(get_map_id "$map_name") [ -z "$map_id" ] && { log_err "$map_name map not found. Is XDP loaded?"; exit 1; } bpftool map update id "$map_id" key hex $key_hex value hex 01 00 00 00 00 00 00 00 2>/dev/null # Persist to file for restore on reload local direct_file="/etc/xdp-defense/whitelist/direct.txt" mkdir -p "$(dirname "$direct_file")" grep -qxF "$name" "$direct_file" 2>/dev/null || echo "$name" >> "$direct_file" log_ok "Whitelisted: $name" return fi python3 "$LIB_DIR/xdp_whitelist.py" add "$name" } cmd_whitelist_del() { local name name=$(sanitize_input "$1") || exit 1 [ -z "$name" ] && { log_err "Usage: xdp-defense whitelist del "; exit 1; } # Direct IP/CIDR if [[ "$name" =~ ^[0-9]+\.[0-9]+ ]] || [[ "$name" =~ ^[0-9a-fA-F]*: ]]; then local map_name key_hex if [[ "$name" == *":"* ]]; then map_name="whitelist_v6" key_hex=$(python3 -c "import sys; from xdp_common import cidr_to_key_v6; print(cidr_to_key_v6(sys.argv[1]))" "$name" 2>/dev/null) else map_name="whitelist_v4" key_hex=$(python3 -c "import sys; from xdp_common import cidr_to_key; print(cidr_to_key(sys.argv[1]))" "$name" 2>/dev/null) fi [ -z "$key_hex" ] && { log_err "Invalid CIDR: $name"; exit 1; } [[ "$key_hex" =~ ^[0-9a-f\ ]+$ ]] || { log_err "Invalid key hex"; exit 1; } local map_id map_id=$(get_map_id "$map_name") [ -z "$map_id" ] && { log_err "$map_name map not found"; exit 1; } bpftool map delete id "$map_id" key hex $key_hex 2>/dev/null && log_ok "Removed from whitelist: $name" # Remove from persistence file local direct_file="/etc/xdp-defense/whitelist/direct.txt" if [ -f "$direct_file" ]; then local tmpfile="${direct_file}.tmp.$$" { grep -vxF "$name" "$direct_file" || true; } > "$tmpfile" 2>/dev/null mv "$tmpfile" "$direct_file" fi return fi python3 "$LIB_DIR/xdp_whitelist.py" del "$name" } cmd_whitelist_list() { python3 "$LIB_DIR/xdp_whitelist.py" list } # ==================== DDoS Commands ==================== cmd_ddos_stats() { echo -e "${BOLD}=== DDoS Statistics ===${NC}" python3 -c " from xdp_common import read_percpu_stats stats = read_percpu_stats('global_stats', 5) labels = ['Passed', 'Dropped (blocked)', 'Dropped (rate)', 'Total', 'Errors'] for i, label in enumerate(labels): val = stats.get(i, 0) print(f' {label:20}: {val:>12}') " 2>/dev/null || log_err "Cannot read stats. Is XDP loaded?" } cmd_ddos_top() { local n=${1:-10} [[ "$n" =~ ^[0-9]+$ ]] || n=10 echo -e "${BOLD}=== Top $n IPs by Packet Count ===${NC}" python3 -c " import sys from xdp_common import dump_rate_counters entries = dump_rate_counters('rate_counter_v4', int(sys.argv[1])) if not entries: print(' (empty)') else: print(f' {\"IP\":>18} {\"Packets\":>10} {\"Bytes\":>12}') print(f' {\"-\"*18} {\"-\"*10} {\"-\"*12}') for ip, pkts, bts, _ in entries: print(f' {ip:>18} {pkts:>10} {bts:>12}') entries6 = dump_rate_counters('rate_counter_v6', int(sys.argv[1])) if entries6: print() print(' IPv6:') for ip, pkts, bts, _ in entries6: print(f' {ip:>42} {pkts:>10} {bts:>12}') " "$n" 2>/dev/null || log_err "Cannot read rate counters. Is XDP loaded?" } cmd_ddos_blocked() { echo -e "${BOLD}=== Blocked IPs ===${NC}" python3 -c " from xdp_common import dump_blocked_ips with open('/proc/uptime') as f: now_ns = int(float(f.read().split()[0]) * 1_000_000_000) entries = dump_blocked_ips('blocked_ips_v4') entries6 = dump_blocked_ips('blocked_ips_v6') all_entries = entries + entries6 if not all_entries: print(' (none)') else: print(f' {\"IP\":>18} {\"Drops\":>10} {\"Expires\":>20}') print(f' {\"-\"*18} {\"-\"*10} {\"-\"*20}') for ip, expire, blocked, drops in all_entries: if expire == 0: exp_str = 'permanent' else: remain = max(0, (expire - now_ns) // 1_000_000_000) if remain > 3600: exp_str = f'{remain // 3600}h {(remain % 3600) // 60}m' elif remain > 60: exp_str = f'{remain // 60}m {remain % 60}s' else: exp_str = f'{remain}s' print(f' {ip:>18} {drops:>10} {exp_str:>20}') " 2>/dev/null || log_err "Cannot read blocked IPs. Is XDP loaded?" } cmd_ddos_block() { local ip ip=$(sanitize_input "$1") || exit 1 local duration=${2:-0} [ -z "$ip" ] && { log_err "Usage: xdp-defense ddos block [duration_sec]"; exit 1; } [[ "$duration" =~ ^[0-9]+$ ]] || { log_err "Invalid duration: $duration"; exit 1; } python3 -c "import sys; from xdp_common import block_ip; block_ip(sys.argv[1], int(sys.argv[2]))" "$ip" "$duration" 2>/dev/null || \ { log_err "Failed to block $ip"; exit 1; } if [ "$duration" -gt 0 ] 2>/dev/null; then log_ok "Blocked $ip for ${duration}s" else log_ok "Blocked $ip (permanent)" fi } cmd_ddos_unblock() { local ip ip=$(sanitize_input "$1") || exit 1 [ -z "$ip" ] && { log_err "Usage: xdp-defense ddos unblock "; exit 1; } python3 -c "import sys; from xdp_common import unblock_ip; unblock_ip(sys.argv[1])" "$ip" 2>/dev/null || \ { log_err "Failed to unblock $ip"; exit 1; } log_ok "Unblocked $ip" } cmd_ddos_config() { local subcmd=${1:-show} case "$subcmd" in show) echo -e "${BOLD}=== Rate Configuration ===${NC}" echo -e "\n${CYAN}Active (BPF map):${NC}" python3 -c " from xdp_common import read_rate_config cfg = read_rate_config() if cfg: pps = cfg['pps_threshold'] bps = cfg['bps_threshold'] win = cfg['window_ns'] print(f' PPS threshold: {pps}') print(f' BPS threshold: {bps if bps > 0 else \"disabled\"}') print(f' Window: {win // 1_000_000_000}s ({win}ns)') else: print(' (not loaded)') " 2>/dev/null if [ -f "$CONFIG_FILE" ]; then echo -e "\n${CYAN}Config file ($CONFIG_FILE):${NC}" python3 -c " import yaml, sys with open(sys.argv[1]) as f: cfg = yaml.safe_load(f) rl = cfg.get('rate_limits', {}) print(f' Default PPS: {rl.get(\"default_pps\", \"N/A\")}') print(f' Default BPS: {rl.get(\"default_bps\", \"N/A\")}') print(f' Window: {rl.get(\"window_sec\", \"N/A\")}s') profiles = rl.get('profiles', {}) if profiles: print() print(' Time profiles:') for name, p in profiles.items(): hours = p.get('hours', '') pps = p.get('pps', 'N/A') print(f' {name}: pps={pps}, hours={hours}') " "$CONFIG_FILE" 2>/dev/null fi ;; set) local key=$2 local value=$3 [ -z "$key" ] || [ -z "$value" ] && { log_err "Usage: xdp-defense ddos config set "; exit 1; } [[ "$key" =~ ^(pps|bps|window)$ ]] || { log_err "Unknown key: $key (must be pps|bps|window)"; exit 1; } [[ "$value" =~ ^[0-9]+$ ]] || { log_err "Invalid value: $value (must be integer)"; exit 1; } python3 -c " import sys from xdp_common import read_rate_config, write_rate_config cfg = read_rate_config() if not cfg: cfg = {'pps_threshold': 1000, 'bps_threshold': 0, 'window_ns': 1000000000} key, value = sys.argv[1], int(sys.argv[2]) if key == 'pps': cfg['pps_threshold'] = value elif key == 'bps': cfg['bps_threshold'] = value elif key == 'window': cfg['window_ns'] = value * 1000000000 write_rate_config(cfg['pps_threshold'], cfg['bps_threshold'], cfg['window_ns']) " "$key" "$value" 2>/dev/null || { log_err "Failed to set config"; exit 1; } log_ok "Set $key=$value" ;; *) log_err "Unknown config command: $subcmd" ;; esac } cmd_ddos_config_apply() { local quiet=$1 [ ! -f "$CONFIG_FILE" ] && return python3 -c " import yaml, sys from xdp_common import write_rate_config with open(sys.argv[1]) as f: cfg = yaml.safe_load(f) rl = cfg.get('rate_limits', {}) pps = rl.get('default_pps', 1000) bps = rl.get('default_bps', 0) win = rl.get('window_sec', 1) write_rate_config(pps, bps, win * 1000000000) " "$CONFIG_FILE" 2>/dev/null || return 0 [ "$quiet" != "quiet" ] && log_ok "Config applied from $CONFIG_FILE" || true } # ==================== Daemon Commands ==================== cmd_daemon_start() { if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then log_err "Daemon already running (PID $(cat "$PID_FILE"))" return 1 fi log_info "Starting defense daemon..." python3 "$(which xdp-defense-daemon 2>/dev/null || echo "$LIB_DIR/xdp_defense_daemon.py")" & log_ok "Daemon started" } cmd_daemon_start_foreground() { # For systemd ExecStart exec python3 "$(which xdp-defense-daemon 2>/dev/null || echo "$LIB_DIR/xdp_defense_daemon.py")" } cmd_daemon_stop() { if [ ! -f "$PID_FILE" ]; then log_err "Daemon not running (no PID file)" return 1 fi local pid pid=$(cat "$PID_FILE") if kill -0 "$pid" 2>/dev/null; then kill -TERM "$pid" local i=0 while [ $i -lt 50 ] && kill -0 "$pid" 2>/dev/null; do sleep 0.1 i=$((i + 1)) done if kill -0 "$pid" 2>/dev/null; then kill -9 "$pid" 2>/dev/null || true fi log_ok "Daemon stopped" else log_info "Daemon not running (stale PID file)" fi rm -f "$PID_FILE" } cmd_daemon_restart() { cmd_daemon_stop 2>/dev/null || true sleep 1 cmd_daemon_start } # ==================== AI Commands ==================== cmd_ai_status() { local model_file="$DATA_DIR/ai_model.pkl" if [ -f "$model_file" ]; then local age=$(( ($(date +%s) - $(stat -c %Y "$model_file")) / 3600 )) echo -e "AI Model: ${GREEN}TRAINED${NC} (${age}h ago)" else echo -e "AI Model: ${YELLOW}NOT TRAINED${NC} (in learning mode)" fi if [ -f "$CONFIG_FILE" ]; then python3 -c " import yaml, sys with open(sys.argv[1]) as f: cfg = yaml.safe_load(f) ai = cfg.get('ai', {}) enabled = ai.get('enabled', False) if enabled: print(f'AI Detection: enabled ({ai.get(\"model_type\", \"IsolationForest\")})') else: print('AI Detection: disabled') " "$CONFIG_FILE" 2>/dev/null fi } cmd_ai_retrain() { log_info "Triggering AI model retrain..." if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then kill -USR1 "$(cat "$PID_FILE")" log_ok "Retrain signal sent to daemon" else log_err "Daemon not running. Start daemon first." exit 1 fi } cmd_ai_traffic() { local db_file db_file=$(python3 -c " import yaml, sys with open(sys.argv[1]) as f: cfg = yaml.safe_load(f) print(cfg.get('ai',{}).get('traffic_log_db', '/var/lib/xdp-defense/traffic_log.db')) " "$CONFIG_FILE" 2>/dev/null || echo "/var/lib/xdp-defense/traffic_log.db") [ ! -f "$db_file" ] && { log_err "Traffic log not found: $db_file"; exit 1; } python3 -c " import sqlite3, sys from datetime import datetime, timedelta db_file = sys.argv[1] cutoff = (datetime.now() - timedelta(hours=24)).isoformat() conn = sqlite3.connect(db_file) cur = conn.cursor() # Buckets: 0-6, 6-12, 12-18, 18-24 buckets = {0: [], 1: [], 2: [], 3: []} total_samples = 0 cur.execute('SELECT hour, total_packets, total_bytes FROM traffic_samples WHERE timestamp >= ?', (cutoff,)) for row in cur.fetchall(): try: hour = float(row[0]) bucket = min(int(hour // 6), 3) pps = float(row[1]) bps = float(row[2]) buckets[bucket].append((pps, bps)) total_samples += 1 except (ValueError, TypeError): continue labels = ['00:00-06:00', '06:00-12:00', '12:00-18:00', '18:00-24:00'] print() print('\033[1m=== Traffic Summary (last 24h) ===\033[0m') print(f'{\"Period\":>15} {\"Avg PPS\":>10} {\"Peak PPS\":>10} {\"Avg BPS\":>12} {\"Samples\":>8}') print(f'{\"-\"*15} {\"-\"*10} {\"-\"*10} {\"-\"*12} {\"-\"*8}') for i, label in enumerate(labels): data = buckets[i] if not data: print(f'{label:>15} {\"--\":>10} {\"--\":>10} {\"--\":>12} {0:>8}') continue pps_list = [d[0] for d in data] bps_list = [d[1] for d in data] avg_pps = sum(pps_list) / len(pps_list) peak_pps = max(pps_list) avg_bps = sum(bps_list) / len(bps_list) def fmt_bytes(b): if b >= 1024*1024: return f'{b/1024/1024:.1f}MB' elif b >= 1024: return f'{b/1024:.1f}KB' return f'{b:.0f}B' print(f'{label:>15} {avg_pps:>10.0f} {peak_pps:>10.0f} {fmt_bytes(avg_bps):>12} {len(data):>8}') hours = total_samples * 5 / 3600 # 5s intervals print(f'Total: {total_samples} samples ({hours:.1f}h)') conn.close() # Show next retrain time import yaml, os, time try: with open(sys.argv[2]) as f: cfg = yaml.safe_load(f) retrain_interval = cfg.get('ai',{}).get('retrain_interval', 86400) model_file = cfg.get('ai',{}).get('model_file', '/var/lib/xdp-defense/ai_model.pkl') if os.path.exists(model_file): mtime = os.path.getmtime(model_file) next_retrain = mtime + retrain_interval - time.time() if next_retrain > 0: h = int(next_retrain // 3600) m = int((next_retrain % 3600) // 60) print(f'Next retrain: {h}h {m}m') else: print('Next retrain: imminent') else: print('Next retrain: model not yet trained') except: pass print() " "$db_file" "$CONFIG_FILE" } cmd_ai_log() { local n=${1:-20} [[ "$n" =~ ^[0-9]+$ ]] || n=20 local db_file db_file=$(python3 -c " import yaml, sys with open(sys.argv[1]) as f: cfg = yaml.safe_load(f) print(cfg.get('ai',{}).get('traffic_log_db', '/var/lib/xdp-defense/traffic_log.db')) " "$CONFIG_FILE" 2>/dev/null || echo "/var/lib/xdp-defense/traffic_log.db") [ ! -f "$db_file" ] && { log_err "Traffic log not found: $db_file"; exit 1; } python3 -c " import sqlite3, sys db_file = sys.argv[1] n = int(sys.argv[2]) conn = sqlite3.connect(db_file) cur = conn.cursor() cur.execute('SELECT COUNT(*) FROM traffic_samples') total_count = cur.fetchone()[0] if total_count == 0: print('Traffic log is empty') conn.close() sys.exit(0) cur.execute('SELECT timestamp, hour, total_packets, total_bytes, syn_ratio, udp_ratio, icmp_ratio FROM traffic_samples ORDER BY id DESC LIMIT ?', (n,)) rows = cur.fetchall() rows.reverse() conn.close() print() print('\033[1m=== Recent Traffic Log ===\033[0m') print(f'{\"Timestamp\":>22} {\"Hour\":>6} {\"PPS\":>10} {\"Bytes\":>12} {\"SYN%\":>6} {\"UDP%\":>6} {\"ICMP%\":>6}') print(f'{\"-\"*22} {\"-\"*6} {\"-\"*10} {\"-\"*12} {\"-\"*6} {\"-\"*6} {\"-\"*6}') for row in rows: try: ts = str(row[0])[:19] # trim microseconds hour = float(row[1]) pkts = float(row[2]) bts = float(row[3]) syn_r = float(row[4]) * 100 if row[4] is not None else 0 udp_r = float(row[5]) * 100 if row[5] is not None else 0 icmp_r = float(row[6]) * 100 if row[6] is not None else 0 print(f'{ts:>22} {hour:>6.1f} {pkts:>10.0f} {bts:>12.0f} {syn_r:>5.1f}% {udp_r:>5.1f}% {icmp_r:>5.1f}%') except (ValueError, TypeError): continue print(f'Showing {len(rows)} of {total_count} entries') print() " "$db_file" "$n" } # ==================== GeoIP ==================== cmd_geoip() { local ip ip=$(sanitize_input "$1") || exit 1 [ -z "$ip" ] && { log_err "Usage: xdp-defense geoip "; exit 1; } echo "=== GeoIP Lookup: $ip ===" if [ -f "$CITY_DB" ]; then local country=$(mmdblookup --file "$CITY_DB" --ip "$ip" country names en 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"') local iso=$(mmdblookup --file "$CITY_DB" --ip "$ip" country iso_code 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"') echo "Country: $country ($iso)" local city=$(mmdblookup --file "$CITY_DB" --ip "$ip" city names en 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"') local region=$(mmdblookup --file "$CITY_DB" --ip "$ip" subdivisions 0 names en 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"') local postal=$(mmdblookup --file "$CITY_DB" --ip "$ip" postal code 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"') local lat=$(mmdblookup --file "$CITY_DB" --ip "$ip" location latitude 2>/dev/null | grep -oP '[0-9.]+') local lon=$(mmdblookup --file "$CITY_DB" --ip "$ip" location longitude 2>/dev/null | grep -oP '[-0-9.]+') local tz=$(mmdblookup --file "$CITY_DB" --ip "$ip" location time_zone 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"') [ -n "$city" ] && echo "City: $city" [ -n "$region" ] && echo "Region: $region" [ -n "$postal" ] && echo "Postal: $postal" [ -n "$lat" ] && [ -n "$lon" ] && echo "Location: $lat, $lon" [ -n "$tz" ] && echo "Timezone: $tz" elif [ -f "$GEOIP_DB" ]; then local country=$(mmdblookup --file "$GEOIP_DB" --ip "$ip" country names en 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"') local iso=$(mmdblookup --file "$GEOIP_DB" --ip "$ip" country iso_code 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"') echo "Country: $country ($iso)" else echo "Country: (GeoIP DB not found)" fi if [ -f "$ASN_DB" ]; then local asn=$(mmdblookup --file "$ASN_DB" --ip "$ip" autonomous_system_number 2>/dev/null | grep -oP '\d+') local org=$(mmdblookup --file "$ASN_DB" --ip "$ip" autonomous_system_organization 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"') echo "ASN: AS$asn" echo "Org: $org" fi # Check if blocked (CIDR blocklist) local map_id map_id=$(get_map_id blocklist_v4) if [ -n "$map_id" ]; then IFS='.' read -r a b c d <<< "$ip" local key_hex key_hex=$(printf '20 00 00 00 %02x %02x %02x %02x' "$a" "$b" "$c" "$d") [[ "$key_hex" =~ ^[0-9a-f\ ]+$ ]] || { log_err "Invalid key hex"; exit 1; } if bpftool map lookup id "$map_id" key hex $key_hex 2>/dev/null | grep -q "value"; then echo -e "Blocker: ${RED}BLOCKED${NC}" else echo -e "Blocker: ${GREEN}ALLOWED${NC}" fi fi # Check if DDoS blocked local ddos_map_id ddos_map_id=$(get_map_id blocked_ips_v4) if [ -n "$ddos_map_id" ]; then local ddos_key_hex ddos_key_hex=$(python3 -c "import sys; from xdp_common import ip_to_hex_key; print(ip_to_hex_key(sys.argv[1]))" "$ip" 2>/dev/null) [[ "$ddos_key_hex" =~ ^[0-9a-f\ ]+$ ]] || { log_err "Invalid key hex"; exit 1; } if [ -n "$ddos_key_hex" ] && bpftool map lookup id "$ddos_map_id" key hex $ddos_key_hex 2>/dev/null | grep -q "value"; then echo -e "DDoS: ${RED}BLOCKED${NC}" else echo -e "DDoS: ${GREEN}ALLOWED${NC}" fi fi } # ==================== Stop All ==================== cmd_stop_all() { local iface=${1:-$(get_iface)} cmd_daemon_stop 2>/dev/null || true cmd_unload "$iface" 2>/dev/null || true log_ok "All stopped" } # ==================== Help ==================== cmd_help() { cat << 'EOF' XDP Defense - Unified XDP Blocker + DDoS Defense High-performance packet filtering with CIDR/country blocking and adaptive rate limiting Usage: xdp-defense [subcommand] [args] System: load [iface] Load both XDP programs via xdp-loader (default: config interface) unload [iface] Unload all XDP programs status Show overall status build Rebuild XDP programs stop-all [iface] Stop daemon and unload XDP Blocker (CIDR/IP): blocker add Add to blocklist blocker del Remove from blocklist blocker list List blocked CIDRs blocker stats Show blocker statistics blocker enable/disable Toggle blocker filtering Country: country add Block a country (e.g., br, cn, ru) country del Unblock a country country list List blocked countries Whitelist: whitelist add Add preset (cloudflare, aws, google, github) whitelist del Remove from whitelist whitelist list List whitelisted services DDoS: ddos stats Show DDoS statistics ddos top [N] Show top N IPs by packet count ddos blocked Show blocked IPs with expiry ddos block [sec] Block IP (optional duration, 0=permanent) ddos unblock Unblock IP ddos config [show|set ] Manage rate config AI: ai status Show AI model status ai retrain Trigger AI model retrain ai traffic Show time-of-day traffic summary (last 24h) ai log [N] Show recent N traffic log entries (default 20) Daemon: daemon start Start defense daemon (background) daemon stop Stop defense daemon daemon restart Restart defense daemon GeoIP: geoip Look up IP geolocation and block status Examples: xdp-defense load xdp-defense blocker add 138.121.244.0/22 xdp-defense country add br xdp-defense whitelist add cloudflare xdp-defense ddos block 192.168.1.100 3600 xdp-defense daemon start xdp-defense status EOF } # ==================== Backward Compatibility ==================== # If invoked as xdp-block, map to blocker subcommands SCRIPT_NAME=$(basename "$0") if [ "$SCRIPT_NAME" = "xdp-block" ]; then case "${1:-help}" in add) cmd_blocker_add "$2" "$3" ;; del) cmd_blocker_del "$2" ;; list) cmd_blocker_list ;; stats) cmd_blocker_stats ;; enable) cmd_blocker_enable ;; disable) cmd_blocker_disable ;; status) cmd_status ;; load) cmd_load "$2" ;; unload) cmd_unload "$2" ;; build) cmd_build ;; country-add) cmd_country_add "$2" ;; country-del) cmd_country_del "$2" ;; country-list) cmd_country_list ;; geoip) cmd_geoip "$2" ;; *) cmd_help ;; esac exit 0 fi # ==================== Main ==================== mkdir -p "$DATA_DIR" 2>/dev/null || true mkdir -p "$(dirname "$BLOCKLIST_FILE")" 2>/dev/null || true case "${1:-help}" in load) cmd_load "$2" ;; unload) cmd_unload "$2" ;; build) cmd_build ;; status) cmd_status ;; stop-all) cmd_stop_all "$2" ;; blocker) case "${2:-help}" in add) cmd_blocker_add "$3" "$4" ;; del) cmd_blocker_del "$3" ;; list) cmd_blocker_list ;; stats) cmd_blocker_stats ;; enable) cmd_blocker_enable ;; disable) cmd_blocker_disable ;; *) echo "Usage: xdp-defense blocker {add|del|list|stats|enable|disable}" ;; esac ;; country) case "${2:-help}" in add) cmd_country_add "$3" ;; del) cmd_country_del "$3" ;; list) cmd_country_list ;; *) echo "Usage: xdp-defense country {add|del|list}" ;; esac ;; whitelist) case "${2:-help}" in add) cmd_whitelist_add "$3" ;; del) cmd_whitelist_del "$3" ;; list) cmd_whitelist_list ;; *) echo "Usage: xdp-defense whitelist {add|del|list}" ;; esac ;; ddos) case "${2:-help}" in stats) cmd_ddos_stats ;; top) cmd_ddos_top "$3" ;; blocked) cmd_ddos_blocked ;; block) cmd_ddos_block "$3" "$4" ;; unblock) cmd_ddos_unblock "$3" ;; config) cmd_ddos_config "$3" "$4" "$5" ;; *) echo "Usage: xdp-defense ddos {stats|top|blocked|block|unblock|config}" ;; esac ;; ai) case "${2:-status}" in status) cmd_ai_status ;; retrain) cmd_ai_retrain ;; traffic) cmd_ai_traffic ;; log) cmd_ai_log "$3" ;; *) cmd_ai_status ;; esac ;; daemon) case "${2:-help}" in start) cmd_daemon_start ;; start-foreground) cmd_daemon_start_foreground ;; stop) cmd_daemon_stop ;; restart) cmd_daemon_restart ;; *) echo "Usage: xdp-defense daemon {start|stop|restart}" ;; esac ;; geoip) cmd_geoip "$2" ;; *) cmd_help ;; esac