Unify xdp-blocker and xdp-ddos into single xdp-defense project
Chain two XDP programs via libxdp dispatcher on the same interface: xdp_blocker (priority 10) handles CIDR/country/whitelist blocking, xdp_ddos (priority 20) handles rate limiting, EWMA analysis, and AI anomaly detection. Whitelist maps are shared via BPF map pinning so whitelisted IPs bypass both blocklist checks and DDoS rate limiting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
971
bin/xdp-defense
Executable file
971
bin/xdp-defense
Executable file
@@ -0,0 +1,971 @@
|
||||
#!/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>"; exit 1; }
|
||||
python3 "$LIB_DIR/xdp_whitelist.py" add "$name"
|
||||
}
|
||||
|
||||
cmd_whitelist_del() {
|
||||
local name=$1
|
||||
[ -z "$name" ] && { log_err "Usage: xdp-defense whitelist del <name>"; exit 1; }
|
||||
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
|
||||
12
bin/xdp-startup.sh
Executable file
12
bin/xdp-startup.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
# XDP Defense - Boot Restore Script
|
||||
# Restores XDP programs and data on system startup
|
||||
# Called by systemd ExecStartPre or manually
|
||||
set -e
|
||||
|
||||
DEFENSE_CMD="/usr/local/bin/xdp-defense"
|
||||
|
||||
# Load XDP programs (blocker + ddos) via xdp-loader
|
||||
"$DEFENSE_CMD" load
|
||||
|
||||
echo "XDP Defense boot restore complete"
|
||||
Reference in New Issue
Block a user