Files
xdp-defense/bin/xdp-defense
kaffa 4ae4440504 Unify legacy data path /etc/xdp-blocker → /etc/xdp-defense
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>
2026-02-07 16:40:46 +09:00

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