Fix 12 code review issues (4 MEDIUM + 8 LOW)

MEDIUM:
- M1: Whitelist direct IP/CIDR additions now persist to direct.txt
- M2: get_map_id() uses 5s TTL cache (single bpftool call for all maps)
- M3: IPv6 extension header parsing in xdp_ddos.c (hop-by-hop/routing/frag/dst)
- M4: Shell injection prevention - sanitize_input() + sys.argv[] for all Python calls

LOW:
- L1: Remove redundant self.running (uses _stop_event only)
- L2: Remove unused config values (rate_limit_after, cooldown_multiplier, retrain_interval)
- L3: Thread poll intervals reloaded on SIGHUP
- L4: batch_map_operation counts only successfully written entries
- L5: Clarify unique_ips_approx comment (per-packet counter)
- L6: Document LRU_HASH multi-CPU race condition as acceptable
- L7: Download Cloudflare IPv6 ranges in whitelist preset
- L8: Fix file handle leak in xdp_country.py list_countries()

Also: SIGHUP now preserves EWMA/violation state, daemon skips whitelisted
IPs in EWMA/AI escalation, deep copy for default config, IHL validation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
kaffa
2026-02-07 09:23:41 +09:00
parent dbfcb62cdf
commit 667c6eac81
7 changed files with 218 additions and 67 deletions

View File

@@ -2,8 +2,6 @@
# 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"
@@ -32,6 +30,16 @@ log_ok() { echo -e "${GREEN}[OK]${NC} $1"; logger -t xdp-defense "OK: $1" 2>/d
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 for safe embedding in Python strings (reject dangerous chars)
sanitize_input() {
local val="$1"
if [[ "$val" =~ [\'\"\;\$\`\\] ]]; then
log_err "Invalid characters in input: $val"
return 1
fi
echo "$val"
}
get_iface() {
if [ -f "$CONFIG_FILE" ]; then
local iface
@@ -265,7 +273,8 @@ print(f'Blocked IPs: {len(v4) + len(v6)}')
# ==================== Blocker Commands ====================
cmd_blocker_add() {
local cidr=$1
local cidr
cidr=$(sanitize_input "$1") || exit 1
[ -z "$cidr" ] && { log_err "Usage: xdp-defense blocker add <ip/cidr>"; exit 1; }
if [[ "$cidr" == *":"* ]]; then
@@ -274,7 +283,7 @@ cmd_blocker_add() {
[ -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)
key_hex=$(python3 -c "import sys; from ${COMMON_PY} 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; }
bpftool map update id "$map_id" key hex $key_hex value hex 01 00 00 00 00 00 00 00 2>/dev/null
@@ -312,7 +321,8 @@ cmd_blocker_add() {
}
cmd_blocker_del() {
local cidr=$1
local cidr
cidr=$(sanitize_input "$1") || exit 1
[ -z "$cidr" ] && { log_err "Usage: xdp-defense blocker del <ip/cidr>"; exit 1; }
if [[ "$cidr" == *":"* ]]; then
@@ -321,7 +331,7 @@ cmd_blocker_del() {
[ -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)
key_hex=$(python3 -c "import sys; from ${COMMON_PY} 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; }
bpftool map delete id "$map_id" key hex $key_hex 2>/dev/null && log_ok "Removed (v6): $cidr"
@@ -442,7 +452,8 @@ cmd_country_list() {
# ==================== Whitelist Commands ====================
cmd_whitelist_add() {
local name=$1
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
@@ -450,10 +461,10 @@ cmd_whitelist_add() {
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)
key_hex=$(python3 -c "import sys; from ${COMMON_PY} 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 "from ${COMMON_PY} import cidr_to_key; print(cidr_to_key('$name'))" 2>/dev/null)
key_hex=$(python3 -c "import sys; from ${COMMON_PY} 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; }
@@ -462,6 +473,12 @@ cmd_whitelist_add() {
[ -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-blocker/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
@@ -470,7 +487,8 @@ cmd_whitelist_add() {
}
cmd_whitelist_del() {
local name=$1
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
@@ -478,10 +496,10 @@ cmd_whitelist_del() {
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)
key_hex=$(python3 -c "import sys; from ${COMMON_PY} 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 "from ${COMMON_PY} import cidr_to_key; print(cidr_to_key('$name'))" 2>/dev/null)
key_hex=$(python3 -c "import sys; from ${COMMON_PY} 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; }
@@ -490,6 +508,15 @@ cmd_whitelist_del() {
[ -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-blocker/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
@@ -516,10 +543,12 @@ for i, label in enumerate(labels):
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 ${COMMON_PY} import dump_rate_counters
entries = dump_rate_counters('rate_counter_v4', $n)
entries = dump_rate_counters('rate_counter_v4', int(sys.argv[1]))
if not entries:
print(' (empty)')
else:
@@ -528,13 +557,13 @@ else:
for ip, pkts, bts, _ in entries:
print(f' {ip:>18} {pkts:>10} {bts:>12}')
entries6 = dump_rate_counters('rate_counter_v6', $n)
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}')
" 2>/dev/null || log_err "Cannot read rate counters. Is XDP loaded?"
" "$n" 2>/dev/null || log_err "Cannot read rate counters. Is XDP loaded?"
}
cmd_ddos_blocked() {
@@ -569,11 +598,13 @@ else:
}
cmd_ddos_block() {
local ip=$1
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 "from ${COMMON_PY} import block_ip; block_ip('$ip', $duration)" 2>/dev/null || \
python3 -c "import sys; from ${COMMON_PY} 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
@@ -584,10 +615,11 @@ cmd_ddos_block() {
}
cmd_ddos_unblock() {
local ip=$1
local ip
ip=$(sanitize_input "$1") || exit 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 || \
python3 -c "import sys; from ${COMMON_PY} import unblock_ip; unblock_ip(sys.argv[1])" "$ip" 2>/dev/null || \
{ log_err "Failed to unblock $ip"; exit 1; }
log_ok "Unblocked $ip"
}
@@ -637,25 +669,24 @@ if profiles:
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 ${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')
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
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; }
" "$key" "$value" 2>/dev/null || { log_err "Failed to set config"; exit 1; }
log_ok "Set $key=$value"
;;
*)
@@ -773,7 +804,8 @@ cmd_ai_retrain() {
# ==================== GeoIP ====================
cmd_geoip() {
local ip=$1
local ip
ip=$(sanitize_input "$1") || exit 1
[ -z "$ip" ] && { log_err "Usage: xdp-defense geoip <ip>"; exit 1; }
echo "=== GeoIP Lookup: $ip ==="
@@ -829,7 +861,7 @@ cmd_geoip() {
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)
ddos_key_hex=$(python3 -c "import sys; from ${COMMON_PY} import ip_to_hex_key; print(ip_to_hex_key(sys.argv[1]))" "$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