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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user