Previously whitelist only accepted named presets (cloudflare, aws, etc). Now `xdp-defense whitelist add 8.8.8.8/32` works directly for both IPv4 and IPv6 addresses, writing to the shared pinned BPF map. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1015 lines
32 KiB
Bash
Executable File
1015 lines
32 KiB
Bash
Executable File
#!/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
|
|
set -e
|
|
|
|
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-blocker/blocklist.txt"
|
|
COUNTRY_DIR="/etc/xdp-blocker/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"
|
|
COMMON_PY="xdp_common"
|
|
|
|
# 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; }
|
|
|
|
get_iface() {
|
|
if [ -f "$CONFIG_FILE" ]; then
|
|
local iface
|
|
iface=$(python3 -c "
|
|
import yaml
|
|
with open('$CONFIG_FILE') as f:
|
|
print(yaml.safe_load(f).get('general',{}).get('interface','eth0'))
|
|
" 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-blocker/whitelist" ]; then
|
|
for wl_file in /etc/xdp-blocker/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 ${COMMON_PY} 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 ${COMMON_PY} 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=$1
|
|
[ -z "$cidr" ] && { log_err "Usage: xdp-defense blocker add <ip/cidr>"; 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 "from ${COMMON_PY} import cidr_to_key_v6; print(cidr_to_key_v6('$cidr'))" 2>/dev/null)
|
|
[ -z "$key_hex" ] && { log_err "Invalid IPv6 CIDR: $cidr"; 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")
|
|
|
|
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=$1
|
|
[ -z "$cidr" ] && { log_err "Usage: xdp-defense blocker del <ip/cidr>"; 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 "from ${COMMON_PY} import cidr_to_key_v6; print(cidr_to_key_v6('$cidr'))" 2>/dev/null)
|
|
[ -z "$key_hex" ] && { log_err "Invalid IPv6 CIDR: $cidr"; 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")
|
|
|
|
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('<I', addr))
|
|
print(f' {ip}/{prefix}')
|
|
" 2>/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 <country_code>"; 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 <country_code>"; 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=$1
|
|
[ -z "$name" ] && { log_err "Usage: xdp-defense whitelist add <preset|ip/cidr>"; 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 "from ${COMMON_PY} import cidr_to_key_v6; print(cidr_to_key_v6('$name'))" 2>/dev/null)
|
|
else
|
|
map_name="whitelist_v4"
|
|
key_hex=$(python3 -c "from ${COMMON_PY} import cidr_to_key; print(cidr_to_key('$name'))" 2>/dev/null)
|
|
fi
|
|
[ -z "$key_hex" ] && { log_err "Invalid CIDR: $name"; 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
|
|
log_ok "Whitelisted: $name"
|
|
return
|
|
fi
|
|
|
|
python3 "$LIB_DIR/xdp_whitelist.py" add "$name"
|
|
}
|
|
|
|
cmd_whitelist_del() {
|
|
local name=$1
|
|
[ -z "$name" ] && { log_err "Usage: xdp-defense whitelist del <name|ip/cidr>"; 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 "from ${COMMON_PY} import cidr_to_key_v6; print(cidr_to_key_v6('$name'))" 2>/dev/null)
|
|
else
|
|
map_name="whitelist_v4"
|
|
key_hex=$(python3 -c "from ${COMMON_PY} import cidr_to_key; print(cidr_to_key('$name'))" 2>/dev/null)
|
|
fi
|
|
[ -z "$key_hex" ] && { log_err "Invalid CIDR: $name"; 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"
|
|
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 ${COMMON_PY} 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}
|
|
echo -e "${BOLD}=== Top $n IPs by Packet Count ===${NC}"
|
|
python3 -c "
|
|
from ${COMMON_PY} import dump_rate_counters
|
|
entries = dump_rate_counters('rate_counter_v4', $n)
|
|
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', $n)
|
|
if entries6:
|
|
print()
|
|
print(' IPv6:')
|
|
for ip, pkts, bts, _ in entries6:
|
|
print(f' {ip:>42} {pkts:>10} {bts:>12}')
|
|
" 2>/dev/null || log_err "Cannot read rate counters. Is XDP loaded?"
|
|
}
|
|
|
|
cmd_ddos_blocked() {
|
|
echo -e "${BOLD}=== Blocked IPs ===${NC}"
|
|
python3 -c "
|
|
from ${COMMON_PY} 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=$1
|
|
local duration=${2:-0}
|
|
[ -z "$ip" ] && { log_err "Usage: xdp-defense ddos block <ip> [duration_sec]"; exit 1; }
|
|
|
|
python3 -c "from ${COMMON_PY} import block_ip; block_ip('$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=$1
|
|
[ -z "$ip" ] && { log_err "Usage: xdp-defense ddos unblock <ip>"; exit 1; }
|
|
|
|
python3 -c "from ${COMMON_PY} import unblock_ip; unblock_ip('$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 ${COMMON_PY} 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
|
|
with open('$CONFIG_FILE') 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}')
|
|
" 2>/dev/null
|
|
fi
|
|
;;
|
|
set)
|
|
local key=$2
|
|
local value=$3
|
|
[ -z "$key" ] || [ -z "$value" ] && { log_err "Usage: xdp-defense ddos config set <pps|bps|window> <value>"; exit 1; }
|
|
|
|
python3 -c "
|
|
from ${COMMON_PY} 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 = '$key'
|
|
value = int('$value')
|
|
if key == 'pps':
|
|
cfg['pps_threshold'] = value
|
|
elif key == 'bps':
|
|
cfg['bps_threshold'] = value
|
|
elif key == 'window':
|
|
cfg['window_ns'] = value * 1000000000
|
|
else:
|
|
print(f'Unknown key: {key}')
|
|
raise SystemExit(1)
|
|
write_rate_config(cfg['pps_threshold'], cfg['bps_threshold'], cfg['window_ns'])
|
|
" 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
|
|
from ${COMMON_PY} import write_rate_config
|
|
with open('$CONFIG_FILE') 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)
|
|
" 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
|
|
with open('$CONFIG_FILE') 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')
|
|
" 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
|
|
}
|
|
|
|
# ==================== GeoIP ====================
|
|
|
|
cmd_geoip() {
|
|
local ip=$1
|
|
[ -z "$ip" ] && { log_err "Usage: xdp-defense geoip <ip>"; 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")
|
|
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 "from ${COMMON_PY} import ip_to_hex_key; print(ip_to_hex_key('$ip'))" 2>/dev/null)
|
|
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 <command> [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 <ip/cidr> Add to blocklist
|
|
blocker del <ip/cidr> Remove from blocklist
|
|
blocker list List blocked CIDRs
|
|
blocker stats Show blocker statistics
|
|
blocker enable/disable Toggle blocker filtering
|
|
|
|
Country:
|
|
country add <cc> Block a country (e.g., br, cn, ru)
|
|
country del <cc> Unblock a country
|
|
country list List blocked countries
|
|
|
|
Whitelist:
|
|
whitelist add <preset> Add preset (cloudflare, aws, google, github)
|
|
whitelist del <name> 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 <ip> [sec] Block IP (optional duration, 0=permanent)
|
|
ddos unblock <ip> Unblock IP
|
|
ddos config [show|set <k> <v>] Manage rate config
|
|
|
|
AI:
|
|
ai status Show AI model status
|
|
ai retrain Trigger AI model retrain
|
|
|
|
Daemon:
|
|
daemon start Start defense daemon (background)
|
|
daemon stop Stop defense daemon
|
|
daemon restart Restart defense daemon
|
|
|
|
GeoIP:
|
|
geoip <ip> 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 ;;
|
|
*) 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
|