All config/data paths now use /etc/xdp-defense/ consistently, eliminating the legacy xdp-blocker directory reference. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1211 lines
40 KiB
Bash
Executable File
1211 lines
40 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
|
|
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 <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 "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 <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 "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('<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
|
|
name=$(sanitize_input "$1") || exit 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 "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 <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 "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 <ip> [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 <ip>"; 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 <pps|bps|window> <value>"; 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 <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")
|
|
[[ "$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 <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
|
|
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 <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 ;;
|
|
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
|