Files
xdp-defense/bin/xdp-defense
kaffa 069d09339c Support direct IP/CIDR in whitelist add/del commands
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>
2026-02-07 08:44:14 +09:00

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