Unify xdp-blocker and xdp-ddos into single xdp-defense project

Chain two XDP programs via libxdp dispatcher on the same interface:
xdp_blocker (priority 10) handles CIDR/country/whitelist blocking,
xdp_ddos (priority 20) handles rate limiting, EWMA analysis, and AI
anomaly detection. Whitelist maps are shared via BPF map pinning so
whitelisted IPs bypass both blocklist checks and DDoS rate limiting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
kaffa
2026-02-07 08:39:21 +09:00
commit 1bcaddce25
12 changed files with 3523 additions and 0 deletions

971
bin/xdp-defense Executable file
View File

@@ -0,0 +1,971 @@
#!/bin/bash
# XDP Defense - Unified CLI for XDP Blocker + DDoS Defense
# Combines CIDR/country/whitelist blocking with rate limiting + AI detection
# Uses libxdp dispatcher for chaining two XDP programs
set -e
PROJ_DIR="/opt/xdp-defense"
BPF_DIR="$PROJ_DIR/bpf"
LIB_DIR="$PROJ_DIR/lib"
PIN_PATH="/sys/fs/bpf/xdp-defense"
CONFIG_FILE="/etc/xdp-defense/config.yaml"
DATA_DIR="/var/lib/xdp-defense"
PID_FILE="$DATA_DIR/daemon.pid"
BLOCKLIST_FILE="/etc/xdp-blocker/blocklist.txt"
COUNTRY_DIR="/etc/xdp-blocker/countries"
GEOIP_DB="/usr/share/GeoIP/GeoLite2-Country.mmdb"
CITY_DB="/usr/share/GeoIP/GeoLite2-City.mmdb"
ASN_DB="/usr/share/GeoIP/GeoLite2-ASN.mmdb"
COMMON_PY="xdp_common"
# Ensure Python can find xdp_common.py (installed to /usr/local/bin)
export PYTHONPATH="/usr/local/bin:${PYTHONPATH:-}"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
log_ok() { echo -e "${GREEN}[OK]${NC} $1"; logger -t xdp-defense "OK: $1" 2>/dev/null || true; }
log_err() { echo -e "${RED}[ERROR]${NC} $1" >&2; logger -t xdp-defense -p user.err "ERROR: $1" 2>/dev/null || true; }
log_info() { echo -e "${CYAN}[INFO]${NC} $1"; logger -t xdp-defense "INFO: $1" 2>/dev/null || true; }
get_iface() {
if [ -f "$CONFIG_FILE" ]; then
local iface
iface=$(python3 -c "
import yaml
with open('$CONFIG_FILE') as f:
print(yaml.safe_load(f).get('general',{}).get('interface','eth0'))
" 2>/dev/null)
echo "${iface:-eth0}"
else
echo "eth0"
fi
}
get_map_id() {
bpftool map show -j 2>/dev/null | python3 -c "
import sys, json
try:
maps = json.load(sys.stdin)
for m in maps:
if m.get('name') == '$1':
print(m['id'])
break
except:
pass
" 2>/dev/null
}
validate_cidr() {
local input=$1
local ip prefix
if [[ "$input" == *"/"* ]]; then
ip="${input%/*}"
prefix="${input#*/}"
else
ip="$input"
prefix=32
fi
if ! [[ "$prefix" =~ ^[0-9]+$ ]] || [ "$prefix" -lt 0 ] || [ "$prefix" -gt 32 ]; then
log_err "Invalid prefix: /$prefix (must be 0-32)"
return 1
fi
IFS='.' read -r a b c d <<< "$ip"
if [ -z "$a" ] || [ -z "$b" ] || [ -z "$c" ] || [ -z "$d" ]; then
log_err "Invalid IP format: $ip"
return 1
fi
for octet in "$a" "$b" "$c" "$d"; do
if ! [[ "$octet" =~ ^[0-9]+$ ]] || [ "$octet" -lt 0 ] || [ "$octet" -gt 255 ]; then
log_err "Invalid IP octet: $octet (must be 0-255)"
return 1
fi
done
return 0
}
# ==================== Load / Unload (xdp-loader) ====================
cmd_load() {
local iface=${1:-$(get_iface)}
[ ! -f "$BPF_DIR/xdp_blocker.o" ] || [ ! -f "$BPF_DIR/xdp_ddos.o" ] && cmd_build
# Unload any existing XDP programs
xdp-loader unload "$iface" --all 2>/dev/null || true
# Create pin path
mkdir -p "$PIN_PATH"
log_info "Loading XDP blocker (priority 10) on $iface..."
xdp-loader load "$iface" "$BPF_DIR/xdp_blocker.o" \
--pin-path "$PIN_PATH"
log_info "Loading XDP DDoS (priority 20) on $iface..."
xdp-loader load "$iface" "$BPF_DIR/xdp_ddos.o" \
--pin-path "$PIN_PATH"
# Enable blocker filtering
cmd_blocker_enable quiet
# Apply rate config
cmd_ddos_config_apply quiet
log_ok "XDP Defense loaded on $iface (2 programs chained)"
# Restore blocklist
[ -f "$BLOCKLIST_FILE" ] && cmd_blocker_restore || true
# Restore blocked countries
if [ -d "$COUNTRY_DIR" ]; then
for cc_file in "$COUNTRY_DIR"/*.txt; do
[ -f "$cc_file" ] || continue
local cc
cc=$(basename "$cc_file" .txt)
log_info "Restoring country: $cc"
python3 "$LIB_DIR/xdp_country.py" add "$cc" >/dev/null 2>&1 || true
done
fi
# Restore whitelists
if [ -d "/etc/xdp-blocker/whitelist" ]; then
for wl_file in /etc/xdp-blocker/whitelist/*.txt; do
[ -f "$wl_file" ] || continue
local name
name=$(basename "$wl_file" .txt)
log_info "Restoring whitelist: $name"
python3 "$LIB_DIR/xdp_whitelist.py" add "$name" >/dev/null 2>&1 || true
done
fi
}
cmd_unload() {
local iface=${1:-$(get_iface)}
xdp-loader unload "$iface" --all 2>/dev/null || true
log_ok "XDP Defense unloaded from $iface"
}
cmd_build() {
echo "Building XDP programs..."
make -C "$PROJ_DIR" build
log_ok "Built: xdp_blocker.o + xdp_ddos.o"
}
cmd_status() {
echo -e "${BOLD}=== XDP Defense Status ===${NC}"
echo ""
# Check xdp-loader status
local iface
iface=$(get_iface)
local loader_status
loader_status=$(xdp-loader status "$iface" 2>/dev/null || true)
if echo "$loader_status" | grep -q "xdp_blocker\|xdp_ddos"; then
echo -e "XDP Programs: ${GREEN}LOADED${NC}"
echo "$loader_status" | grep -E "prog_id|prio" | head -10 | while read -r line; do
echo " $line"
done
else
# Fallback: check bpftool
local blocker_prog
blocker_prog=$(bpftool prog show 2>/dev/null | grep "xdp_blocker")
local ddos_prog
ddos_prog=$(bpftool prog show 2>/dev/null | grep "xdp_ddos")
if [ -n "$blocker_prog" ] && [ -n "$ddos_prog" ]; then
echo -e "XDP Programs: ${GREEN}LOADED (both)${NC}"
elif [ -n "$blocker_prog" ]; then
echo -e "XDP Programs: ${YELLOW}PARTIAL (blocker only)${NC}"
elif [ -n "$ddos_prog" ]; then
echo -e "XDP Programs: ${YELLOW}PARTIAL (ddos only)${NC}"
else
echo -e "XDP Programs: ${RED}NOT LOADED${NC}"
fi
fi
echo -e "Interface: ${GREEN}${iface}${NC}"
# Blocker status
local cfg_id
cfg_id=$(get_map_id config)
if [ -n "$cfg_id" ]; then
local val
val=$(bpftool map lookup id "$cfg_id" key 0 0 0 0 2>/dev/null | grep -oP '"value":\s*1' | head -1)
[ -n "$val" ] && echo -e "Blocker: ${GREEN}ENABLED${NC}" || echo -e "Blocker: ${YELLOW}DISABLED${NC}"
fi
# Map counts
local bl_id wl_id
bl_id=$(get_map_id blocklist_v4)
wl_id=$(get_map_id whitelist_v4)
if [ -n "$wl_id" ]; then
local wl_count
wl_count=$(bpftool map dump id "$wl_id" -j 2>/dev/null | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo 0)
echo "Whitelist: $wl_count entries"
fi
if [ -n "$bl_id" ]; then
local bl_count
bl_count=$(bpftool map dump id "$bl_id" -j 2>/dev/null | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo 0)
echo "Blocklist: $bl_count entries"
fi
# Blocked countries
if [ -d "$COUNTRY_DIR" ] && [ "$(ls -A "$COUNTRY_DIR" 2>/dev/null)" ]; then
echo ""
echo "Countries:"
for cc_file in "$COUNTRY_DIR"/*.txt; do
[ -f "$cc_file" ] || continue
local cc
cc=$(basename "$cc_file" .txt | tr 'a-z' 'A-Z')
local count
count=$(wc -l < "$cc_file")
echo " $cc: $count CIDRs"
done
fi
# Daemon
echo ""
if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
echo -e "Daemon: ${GREEN}RUNNING${NC} (PID $(cat "$PID_FILE"))"
else
echo -e "Daemon: ${RED}STOPPED${NC}"
fi
# Rate config
python3 -c "
from ${COMMON_PY} import read_rate_config
cfg = read_rate_config()
if cfg:
print(f'Rate limit: {cfg[\"pps_threshold\"]} pps (window: {cfg[\"window_ns\"] // 1_000_000_000}s)')
else:
print('Rate limit: (not configured)')
" 2>/dev/null || true
# Blocked IPs
python3 -c "
from ${COMMON_PY} import dump_blocked_ips
v4 = dump_blocked_ips('blocked_ips_v4')
v6 = dump_blocked_ips('blocked_ips_v6')
print(f'Blocked IPs: {len(v4) + len(v6)}')
" 2>/dev/null || true
# AI status
echo ""
cmd_ai_status 2>/dev/null || true
}
# ==================== Blocker Commands ====================
cmd_blocker_add() {
local cidr=$1
[ -z "$cidr" ] && { log_err "Usage: xdp-defense blocker add <ip/cidr>"; exit 1; }
if [[ "$cidr" == *":"* ]]; then
local map_id
map_id=$(get_map_id blocklist_v6)
[ -z "$map_id" ] && { log_err "IPv6 map not found. Is XDP loaded?"; exit 1; }
local key_hex
key_hex=$(python3 -c "from ${COMMON_PY} import cidr_to_key_v6; print(cidr_to_key_v6('$cidr'))" 2>/dev/null)
[ -z "$key_hex" ] && { log_err "Invalid IPv6 CIDR: $cidr"; exit 1; }
bpftool map update id "$map_id" key hex $key_hex value hex 01 00 00 00 00 00 00 00 2>/dev/null
[ "$2" != "quiet" ] && log_ok "Added (v6): $cidr" || true
mkdir -p "$(dirname "$BLOCKLIST_FILE")"
grep -qxF "$cidr" "$BLOCKLIST_FILE" 2>/dev/null || echo "$cidr" >> "$BLOCKLIST_FILE"
return
fi
validate_cidr "$cidr" || exit 1
local ip prefix
if [[ "$cidr" == *"/"* ]]; then
ip="${cidr%/*}"
prefix="${cidr#*/}"
else
ip="$cidr"
prefix=32
fi
local map_id
map_id=$(get_map_id blocklist_v4)
[ -z "$map_id" ] && { log_err "Map not found. Is XDP loaded?"; exit 1; }
IFS='.' read -r a b c d <<< "$ip"
local key_hex
key_hex=$(printf '%02x 00 00 00 %02x %02x %02x %02x' "$prefix" "$a" "$b" "$c" "$d")
bpftool map update id "$map_id" key hex $key_hex value hex 01 00 00 00 00 00 00 00 2>/dev/null
[ "$2" != "quiet" ] && log_ok "Added: $cidr" || true
mkdir -p "$(dirname "$BLOCKLIST_FILE")"
grep -qxF "$cidr" "$BLOCKLIST_FILE" 2>/dev/null || echo "$cidr" >> "$BLOCKLIST_FILE"
}
cmd_blocker_del() {
local cidr=$1
[ -z "$cidr" ] && { log_err "Usage: xdp-defense blocker del <ip/cidr>"; exit 1; }
if [[ "$cidr" == *":"* ]]; then
local map_id
map_id=$(get_map_id blocklist_v6)
[ -z "$map_id" ] && { log_err "IPv6 map not found"; exit 1; }
local key_hex
key_hex=$(python3 -c "from ${COMMON_PY} import cidr_to_key_v6; print(cidr_to_key_v6('$cidr'))" 2>/dev/null)
[ -z "$key_hex" ] && { log_err "Invalid IPv6 CIDR: $cidr"; exit 1; }
bpftool map delete id "$map_id" key hex $key_hex 2>/dev/null && log_ok "Removed (v6): $cidr"
local tmpfile="${BLOCKLIST_FILE}.tmp.$$"
grep -vxF "$cidr" "$BLOCKLIST_FILE" > "$tmpfile" 2>/dev/null && mv "$tmpfile" "$BLOCKLIST_FILE" || rm -f "$tmpfile"
return
fi
validate_cidr "$cidr" || exit 1
local ip prefix
if [[ "$cidr" == *"/"* ]]; then
ip="${cidr%/*}"
prefix="${cidr#*/}"
else
ip="$cidr"
prefix=32
fi
local map_id
map_id=$(get_map_id blocklist_v4)
[ -z "$map_id" ] && { log_err "Map not found"; exit 1; }
IFS='.' read -r a b c d <<< "$ip"
local key_hex
key_hex=$(printf '%02x 00 00 00 %02x %02x %02x %02x' "$prefix" "$a" "$b" "$c" "$d")
bpftool map delete id "$map_id" key hex $key_hex 2>/dev/null && log_ok "Removed: $cidr"
local tmpfile="${BLOCKLIST_FILE}.tmp.$$"
grep -vxF "$cidr" "$BLOCKLIST_FILE" > "$tmpfile" 2>/dev/null && mv "$tmpfile" "$BLOCKLIST_FILE" || rm -f "$tmpfile"
}
cmd_blocker_list() {
local map_id
map_id=$(get_map_id blocklist_v4)
[ -z "$map_id" ] && { log_err "Map not found"; exit 1; }
echo "=== Blocked CIDRs ==="
bpftool map dump id "$map_id" -j 2>/dev/null | \
python3 -c "
import sys, json, socket, struct
data = json.load(sys.stdin)
for entry in data:
fmt = entry.get('formatted', entry)
key = fmt['key']
prefix = key['prefixlen']
addr = key['addr']
ip = socket.inet_ntoa(struct.pack('<I', addr))
print(f' {ip}/{prefix}')
" 2>/dev/null || echo " (empty)"
}
cmd_blocker_stats() {
local map_id
map_id=$(get_map_id stats_map)
[ -z "$map_id" ] && { log_err "Stats map not found"; exit 1; }
echo "=== Blocker Statistics ==="
bpftool map dump id "$map_id" -j 2>/dev/null | \
python3 -c "
import sys, json
data = json.load(sys.stdin)
labels = ['Passed', 'Dropped', 'Whitelist', 'Errors']
for entry in data:
fmt = entry.get('formatted', entry)
key = fmt['key'] if isinstance(fmt['key'], int) else int(fmt['key'][0], 16)
values = fmt.get('values', [])
total_pkts = sum(v['value']['packets'] for v in values)
total_bytes = sum(v['value']['bytes'] for v in values)
if key < len(labels):
print(f'{labels[key]:9}: {total_pkts:>10} pkts, {total_bytes:>12} bytes')
"
}
cmd_blocker_enable() {
local map_id
map_id=$(get_map_id config)
[ -n "$map_id" ] && bpftool map update id "$map_id" key 0 0 0 0 value 1 0 0 0
[ "$1" != "quiet" ] && log_ok "Blocker filtering enabled" || true
}
cmd_blocker_disable() {
local map_id
map_id=$(get_map_id config)
[ -n "$map_id" ] && bpftool map update id "$map_id" key 0 0 0 0 value 0 0 0 0
log_ok "Blocker filtering disabled"
}
cmd_blocker_restore() {
[ ! -f "$BLOCKLIST_FILE" ] && return
local count=0
while read -r cidr; do
[[ -z "$cidr" || "$cidr" == "#"* ]] && continue
cmd_blocker_add "$cidr" quiet 2>/dev/null && ((count++)) || true
done < "$BLOCKLIST_FILE"
log_ok "Restored $count blocklist entries"
}
# ==================== Country Commands ====================
cmd_country_add() {
local cc=$(echo "$1" | tr 'A-Z' 'a-z')
[ -z "$cc" ] && { log_err "Usage: xdp-defense country add <country_code>"; exit 1; }
[[ "$cc" =~ ^[a-z]{2}$ ]] || { log_err "Invalid country code: $cc (must be 2 letters)"; exit 1; }
python3 "$LIB_DIR/xdp_country.py" add "$cc"
}
cmd_country_del() {
local cc=$(echo "$1" | tr 'A-Z' 'a-z')
[ -z "$cc" ] && { log_err "Usage: xdp-defense country del <country_code>"; exit 1; }
python3 "$LIB_DIR/xdp_country.py" del "$cc"
}
cmd_country_list() {
python3 "$LIB_DIR/xdp_country.py" list
}
# ==================== Whitelist Commands ====================
cmd_whitelist_add() {
local name=$1
[ -z "$name" ] && { log_err "Usage: xdp-defense whitelist add <preset>"; exit 1; }
python3 "$LIB_DIR/xdp_whitelist.py" add "$name"
}
cmd_whitelist_del() {
local name=$1
[ -z "$name" ] && { log_err "Usage: xdp-defense whitelist del <name>"; exit 1; }
python3 "$LIB_DIR/xdp_whitelist.py" del "$name"
}
cmd_whitelist_list() {
python3 "$LIB_DIR/xdp_whitelist.py" list
}
# ==================== DDoS Commands ====================
cmd_ddos_stats() {
echo -e "${BOLD}=== DDoS Statistics ===${NC}"
python3 -c "
from ${COMMON_PY} import read_percpu_stats
stats = read_percpu_stats('global_stats', 5)
labels = ['Passed', 'Dropped (blocked)', 'Dropped (rate)', 'Total', 'Errors']
for i, label in enumerate(labels):
val = stats.get(i, 0)
print(f' {label:20}: {val:>12}')
" 2>/dev/null || log_err "Cannot read stats. Is XDP loaded?"
}
cmd_ddos_top() {
local n=${1:-10}
echo -e "${BOLD}=== Top $n IPs by Packet Count ===${NC}"
python3 -c "
from ${COMMON_PY} import dump_rate_counters
entries = dump_rate_counters('rate_counter_v4', $n)
if not entries:
print(' (empty)')
else:
print(f' {\"IP\":>18} {\"Packets\":>10} {\"Bytes\":>12}')
print(f' {\"-\"*18} {\"-\"*10} {\"-\"*12}')
for ip, pkts, bts, _ in entries:
print(f' {ip:>18} {pkts:>10} {bts:>12}')
entries6 = dump_rate_counters('rate_counter_v6', $n)
if entries6:
print()
print(' IPv6:')
for ip, pkts, bts, _ in entries6:
print(f' {ip:>42} {pkts:>10} {bts:>12}')
" 2>/dev/null || log_err "Cannot read rate counters. Is XDP loaded?"
}
cmd_ddos_blocked() {
echo -e "${BOLD}=== Blocked IPs ===${NC}"
python3 -c "
from ${COMMON_PY} import dump_blocked_ips
with open('/proc/uptime') as f:
now_ns = int(float(f.read().split()[0]) * 1_000_000_000)
entries = dump_blocked_ips('blocked_ips_v4')
entries6 = dump_blocked_ips('blocked_ips_v6')
all_entries = entries + entries6
if not all_entries:
print(' (none)')
else:
print(f' {\"IP\":>18} {\"Drops\":>10} {\"Expires\":>20}')
print(f' {\"-\"*18} {\"-\"*10} {\"-\"*20}')
for ip, expire, blocked, drops in all_entries:
if expire == 0:
exp_str = 'permanent'
else:
remain = max(0, (expire - now_ns) // 1_000_000_000)
if remain > 3600:
exp_str = f'{remain // 3600}h {(remain % 3600) // 60}m'
elif remain > 60:
exp_str = f'{remain // 60}m {remain % 60}s'
else:
exp_str = f'{remain}s'
print(f' {ip:>18} {drops:>10} {exp_str:>20}')
" 2>/dev/null || log_err "Cannot read blocked IPs. Is XDP loaded?"
}
cmd_ddos_block() {
local ip=$1
local duration=${2:-0}
[ -z "$ip" ] && { log_err "Usage: xdp-defense ddos block <ip> [duration_sec]"; exit 1; }
python3 -c "from ${COMMON_PY} import block_ip; block_ip('$ip', $duration)" 2>/dev/null || \
{ log_err "Failed to block $ip"; exit 1; }
if [ "$duration" -gt 0 ] 2>/dev/null; then
log_ok "Blocked $ip for ${duration}s"
else
log_ok "Blocked $ip (permanent)"
fi
}
cmd_ddos_unblock() {
local ip=$1
[ -z "$ip" ] && { log_err "Usage: xdp-defense ddos unblock <ip>"; exit 1; }
python3 -c "from ${COMMON_PY} import unblock_ip; unblock_ip('$ip')" 2>/dev/null || \
{ log_err "Failed to unblock $ip"; exit 1; }
log_ok "Unblocked $ip"
}
cmd_ddos_config() {
local subcmd=${1:-show}
case "$subcmd" in
show)
echo -e "${BOLD}=== Rate Configuration ===${NC}"
echo -e "\n${CYAN}Active (BPF map):${NC}"
python3 -c "
from ${COMMON_PY} import read_rate_config
cfg = read_rate_config()
if cfg:
pps = cfg['pps_threshold']
bps = cfg['bps_threshold']
win = cfg['window_ns']
print(f' PPS threshold: {pps}')
print(f' BPS threshold: {bps if bps > 0 else \"disabled\"}')
print(f' Window: {win // 1_000_000_000}s ({win}ns)')
else:
print(' (not loaded)')
" 2>/dev/null
if [ -f "$CONFIG_FILE" ]; then
echo -e "\n${CYAN}Config file ($CONFIG_FILE):${NC}"
python3 -c "
import yaml
with open('$CONFIG_FILE') as f:
cfg = yaml.safe_load(f)
rl = cfg.get('rate_limits', {})
print(f' Default PPS: {rl.get(\"default_pps\", \"N/A\")}')
print(f' Default BPS: {rl.get(\"default_bps\", \"N/A\")}')
print(f' Window: {rl.get(\"window_sec\", \"N/A\")}s')
profiles = rl.get('profiles', {})
if profiles:
print()
print(' Time profiles:')
for name, p in profiles.items():
hours = p.get('hours', '')
pps = p.get('pps', 'N/A')
print(f' {name}: pps={pps}, hours={hours}')
" 2>/dev/null
fi
;;
set)
local key=$2
local value=$3
[ -z "$key" ] || [ -z "$value" ] && { log_err "Usage: xdp-defense ddos config set <pps|bps|window> <value>"; exit 1; }
python3 -c "
from ${COMMON_PY} import read_rate_config, write_rate_config
cfg = read_rate_config()
if not cfg:
cfg = {'pps_threshold': 1000, 'bps_threshold': 0, 'window_ns': 1000000000}
key = '$key'
value = int('$value')
if key == 'pps':
cfg['pps_threshold'] = value
elif key == 'bps':
cfg['bps_threshold'] = value
elif key == 'window':
cfg['window_ns'] = value * 1000000000
else:
print(f'Unknown key: {key}')
raise SystemExit(1)
write_rate_config(cfg['pps_threshold'], cfg['bps_threshold'], cfg['window_ns'])
" 2>/dev/null || { log_err "Failed to set config"; exit 1; }
log_ok "Set $key=$value"
;;
*)
log_err "Unknown config command: $subcmd"
;;
esac
}
cmd_ddos_config_apply() {
local quiet=$1
[ ! -f "$CONFIG_FILE" ] && return
python3 -c "
import yaml
from ${COMMON_PY} import write_rate_config
with open('$CONFIG_FILE') as f:
cfg = yaml.safe_load(f)
rl = cfg.get('rate_limits', {})
pps = rl.get('default_pps', 1000)
bps = rl.get('default_bps', 0)
win = rl.get('window_sec', 1)
write_rate_config(pps, bps, win * 1000000000)
" 2>/dev/null || return 0
[ "$quiet" != "quiet" ] && log_ok "Config applied from $CONFIG_FILE" || true
}
# ==================== Daemon Commands ====================
cmd_daemon_start() {
if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
log_err "Daemon already running (PID $(cat "$PID_FILE"))"
return 1
fi
log_info "Starting defense daemon..."
python3 "$(which xdp-defense-daemon 2>/dev/null || echo "$LIB_DIR/xdp_defense_daemon.py")" &
log_ok "Daemon started"
}
cmd_daemon_start_foreground() {
# For systemd ExecStart
exec python3 "$(which xdp-defense-daemon 2>/dev/null || echo "$LIB_DIR/xdp_defense_daemon.py")"
}
cmd_daemon_stop() {
if [ ! -f "$PID_FILE" ]; then
log_err "Daemon not running (no PID file)"
return 1
fi
local pid
pid=$(cat "$PID_FILE")
if kill -0 "$pid" 2>/dev/null; then
kill -TERM "$pid"
local i=0
while [ $i -lt 50 ] && kill -0 "$pid" 2>/dev/null; do
sleep 0.1
i=$((i + 1))
done
if kill -0 "$pid" 2>/dev/null; then
kill -9 "$pid" 2>/dev/null || true
fi
log_ok "Daemon stopped"
else
log_info "Daemon not running (stale PID file)"
fi
rm -f "$PID_FILE"
}
cmd_daemon_restart() {
cmd_daemon_stop 2>/dev/null || true
sleep 1
cmd_daemon_start
}
# ==================== AI Commands ====================
cmd_ai_status() {
local model_file="$DATA_DIR/ai_model.pkl"
if [ -f "$model_file" ]; then
local age=$(( ($(date +%s) - $(stat -c %Y "$model_file")) / 3600 ))
echo -e "AI Model: ${GREEN}TRAINED${NC} (${age}h ago)"
else
echo -e "AI Model: ${YELLOW}NOT TRAINED${NC} (in learning mode)"
fi
if [ -f "$CONFIG_FILE" ]; then
python3 -c "
import yaml
with open('$CONFIG_FILE') as f:
cfg = yaml.safe_load(f)
ai = cfg.get('ai', {})
enabled = ai.get('enabled', False)
if enabled:
print(f'AI Detection: enabled ({ai.get(\"model_type\", \"IsolationForest\")})')
else:
print('AI Detection: disabled')
" 2>/dev/null
fi
}
cmd_ai_retrain() {
log_info "Triggering AI model retrain..."
if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
kill -USR1 "$(cat "$PID_FILE")"
log_ok "Retrain signal sent to daemon"
else
log_err "Daemon not running. Start daemon first."
exit 1
fi
}
# ==================== GeoIP ====================
cmd_geoip() {
local ip=$1
[ -z "$ip" ] && { log_err "Usage: xdp-defense geoip <ip>"; exit 1; }
echo "=== GeoIP Lookup: $ip ==="
if [ -f "$CITY_DB" ]; then
local country=$(mmdblookup --file "$CITY_DB" --ip "$ip" country names en 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
local iso=$(mmdblookup --file "$CITY_DB" --ip "$ip" country iso_code 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
echo "Country: $country ($iso)"
local city=$(mmdblookup --file "$CITY_DB" --ip "$ip" city names en 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
local region=$(mmdblookup --file "$CITY_DB" --ip "$ip" subdivisions 0 names en 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
local postal=$(mmdblookup --file "$CITY_DB" --ip "$ip" postal code 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
local lat=$(mmdblookup --file "$CITY_DB" --ip "$ip" location latitude 2>/dev/null | grep -oP '[0-9.]+')
local lon=$(mmdblookup --file "$CITY_DB" --ip "$ip" location longitude 2>/dev/null | grep -oP '[-0-9.]+')
local tz=$(mmdblookup --file "$CITY_DB" --ip "$ip" location time_zone 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
[ -n "$city" ] && echo "City: $city"
[ -n "$region" ] && echo "Region: $region"
[ -n "$postal" ] && echo "Postal: $postal"
[ -n "$lat" ] && [ -n "$lon" ] && echo "Location: $lat, $lon"
[ -n "$tz" ] && echo "Timezone: $tz"
elif [ -f "$GEOIP_DB" ]; then
local country=$(mmdblookup --file "$GEOIP_DB" --ip "$ip" country names en 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
local iso=$(mmdblookup --file "$GEOIP_DB" --ip "$ip" country iso_code 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
echo "Country: $country ($iso)"
else
echo "Country: (GeoIP DB not found)"
fi
if [ -f "$ASN_DB" ]; then
local asn=$(mmdblookup --file "$ASN_DB" --ip "$ip" autonomous_system_number 2>/dev/null | grep -oP '\d+')
local org=$(mmdblookup --file "$ASN_DB" --ip "$ip" autonomous_system_organization 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
echo "ASN: AS$asn"
echo "Org: $org"
fi
# Check if blocked (CIDR blocklist)
local map_id
map_id=$(get_map_id blocklist_v4)
if [ -n "$map_id" ]; then
IFS='.' read -r a b c d <<< "$ip"
local key_hex
key_hex=$(printf '20 00 00 00 %02x %02x %02x %02x' "$a" "$b" "$c" "$d")
if bpftool map lookup id "$map_id" key hex $key_hex 2>/dev/null | grep -q "value"; then
echo -e "Blocker: ${RED}BLOCKED${NC}"
else
echo -e "Blocker: ${GREEN}ALLOWED${NC}"
fi
fi
# Check if DDoS blocked
local ddos_map_id
ddos_map_id=$(get_map_id blocked_ips_v4)
if [ -n "$ddos_map_id" ]; then
local ddos_key_hex
ddos_key_hex=$(python3 -c "from ${COMMON_PY} import ip_to_hex_key; print(ip_to_hex_key('$ip'))" 2>/dev/null)
if [ -n "$ddos_key_hex" ] && bpftool map lookup id "$ddos_map_id" key hex $ddos_key_hex 2>/dev/null | grep -q "value"; then
echo -e "DDoS: ${RED}BLOCKED${NC}"
else
echo -e "DDoS: ${GREEN}ALLOWED${NC}"
fi
fi
}
# ==================== Stop All ====================
cmd_stop_all() {
local iface=${1:-$(get_iface)}
cmd_daemon_stop 2>/dev/null || true
cmd_unload "$iface" 2>/dev/null || true
log_ok "All stopped"
}
# ==================== Help ====================
cmd_help() {
cat << 'EOF'
XDP Defense - Unified XDP Blocker + DDoS Defense
High-performance packet filtering with CIDR/country blocking and adaptive rate limiting
Usage: xdp-defense <command> [subcommand] [args]
System:
load [iface] Load both XDP programs via xdp-loader (default: config interface)
unload [iface] Unload all XDP programs
status Show overall status
build Rebuild XDP programs
stop-all [iface] Stop daemon and unload XDP
Blocker (CIDR/IP):
blocker add <ip/cidr> Add to blocklist
blocker del <ip/cidr> Remove from blocklist
blocker list List blocked CIDRs
blocker stats Show blocker statistics
blocker enable/disable Toggle blocker filtering
Country:
country add <cc> Block a country (e.g., br, cn, ru)
country del <cc> Unblock a country
country list List blocked countries
Whitelist:
whitelist add <preset> Add preset (cloudflare, aws, google, github)
whitelist del <name> Remove from whitelist
whitelist list List whitelisted services
DDoS:
ddos stats Show DDoS statistics
ddos top [N] Show top N IPs by packet count
ddos blocked Show blocked IPs with expiry
ddos block <ip> [sec] Block IP (optional duration, 0=permanent)
ddos unblock <ip> Unblock IP
ddos config [show|set <k> <v>] Manage rate config
AI:
ai status Show AI model status
ai retrain Trigger AI model retrain
Daemon:
daemon start Start defense daemon (background)
daemon stop Stop defense daemon
daemon restart Restart defense daemon
GeoIP:
geoip <ip> Look up IP geolocation and block status
Examples:
xdp-defense load
xdp-defense blocker add 138.121.244.0/22
xdp-defense country add br
xdp-defense whitelist add cloudflare
xdp-defense ddos block 192.168.1.100 3600
xdp-defense daemon start
xdp-defense status
EOF
}
# ==================== Backward Compatibility ====================
# If invoked as xdp-block, map to blocker subcommands
SCRIPT_NAME=$(basename "$0")
if [ "$SCRIPT_NAME" = "xdp-block" ]; then
case "${1:-help}" in
add) cmd_blocker_add "$2" "$3" ;;
del) cmd_blocker_del "$2" ;;
list) cmd_blocker_list ;;
stats) cmd_blocker_stats ;;
enable) cmd_blocker_enable ;;
disable) cmd_blocker_disable ;;
status) cmd_status ;;
load) cmd_load "$2" ;;
unload) cmd_unload "$2" ;;
build) cmd_build ;;
country-add) cmd_country_add "$2" ;;
country-del) cmd_country_del "$2" ;;
country-list) cmd_country_list ;;
geoip) cmd_geoip "$2" ;;
*) cmd_help ;;
esac
exit 0
fi
# ==================== Main ====================
mkdir -p "$DATA_DIR" 2>/dev/null || true
mkdir -p "$(dirname "$BLOCKLIST_FILE")" 2>/dev/null || true
case "${1:-help}" in
load) cmd_load "$2" ;;
unload) cmd_unload "$2" ;;
build) cmd_build ;;
status) cmd_status ;;
stop-all) cmd_stop_all "$2" ;;
blocker)
case "${2:-help}" in
add) cmd_blocker_add "$3" "$4" ;;
del) cmd_blocker_del "$3" ;;
list) cmd_blocker_list ;;
stats) cmd_blocker_stats ;;
enable) cmd_blocker_enable ;;
disable) cmd_blocker_disable ;;
*) echo "Usage: xdp-defense blocker {add|del|list|stats|enable|disable}" ;;
esac
;;
country)
case "${2:-help}" in
add) cmd_country_add "$3" ;;
del) cmd_country_del "$3" ;;
list) cmd_country_list ;;
*) echo "Usage: xdp-defense country {add|del|list}" ;;
esac
;;
whitelist)
case "${2:-help}" in
add) cmd_whitelist_add "$3" ;;
del) cmd_whitelist_del "$3" ;;
list) cmd_whitelist_list ;;
*) echo "Usage: xdp-defense whitelist {add|del|list}" ;;
esac
;;
ddos)
case "${2:-help}" in
stats) cmd_ddos_stats ;;
top) cmd_ddos_top "$3" ;;
blocked) cmd_ddos_blocked ;;
block) cmd_ddos_block "$3" "$4" ;;
unblock) cmd_ddos_unblock "$3" ;;
config) cmd_ddos_config "$3" "$4" "$5" ;;
*) echo "Usage: xdp-defense ddos {stats|top|blocked|block|unblock|config}" ;;
esac
;;
ai)
case "${2:-status}" in
status) cmd_ai_status ;;
retrain) cmd_ai_retrain ;;
*) cmd_ai_status ;;
esac
;;
daemon)
case "${2:-help}" in
start) cmd_daemon_start ;;
start-foreground) cmd_daemon_start_foreground ;;
stop) cmd_daemon_stop ;;
restart) cmd_daemon_restart ;;
*) echo "Usage: xdp-defense daemon {start|stop|restart}" ;;
esac
;;
geoip) cmd_geoip "$2" ;;
*) cmd_help ;;
esac

12
bin/xdp-startup.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/bash
# XDP Defense - Boot Restore Script
# Restores XDP programs and data on system startup
# Called by systemd ExecStartPre or manually
set -e
DEFENSE_CMD="/usr/local/bin/xdp-defense"
# Load XDP programs (blocker + ddos) via xdp-loader
"$DEFENSE_CMD" load
echo "XDP Defense boot restore complete"