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
|
# XDP Defense - Unified CLI for XDP Blocker + DDoS Defense
|
||||||
# Combines CIDR/country/whitelist blocking with rate limiting + AI detection
|
# Combines CIDR/country/whitelist blocking with rate limiting + AI detection
|
||||||
# Uses libxdp dispatcher for chaining two XDP programs
|
# Uses libxdp dispatcher for chaining two XDP programs
|
||||||
set -e
|
|
||||||
|
|
||||||
PROJ_DIR="/opt/xdp-defense"
|
PROJ_DIR="/opt/xdp-defense"
|
||||||
BPF_DIR="$PROJ_DIR/bpf"
|
BPF_DIR="$PROJ_DIR/bpf"
|
||||||
LIB_DIR="$PROJ_DIR/lib"
|
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_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; }
|
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() {
|
get_iface() {
|
||||||
if [ -f "$CONFIG_FILE" ]; then
|
if [ -f "$CONFIG_FILE" ]; then
|
||||||
local iface
|
local iface
|
||||||
@@ -265,7 +273,8 @@ print(f'Blocked IPs: {len(v4) + len(v6)}')
|
|||||||
# ==================== Blocker Commands ====================
|
# ==================== Blocker Commands ====================
|
||||||
|
|
||||||
cmd_blocker_add() {
|
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; }
|
[ -z "$cidr" ] && { log_err "Usage: xdp-defense blocker add <ip/cidr>"; exit 1; }
|
||||||
|
|
||||||
if [[ "$cidr" == *":"* ]]; then
|
if [[ "$cidr" == *":"* ]]; then
|
||||||
@@ -274,7 +283,7 @@ cmd_blocker_add() {
|
|||||||
[ -z "$map_id" ] && { log_err "IPv6 map not found. Is XDP loaded?"; exit 1; }
|
[ -z "$map_id" ] && { log_err "IPv6 map not found. Is XDP loaded?"; exit 1; }
|
||||||
|
|
||||||
local key_hex
|
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; }
|
[ -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
|
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() {
|
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; }
|
[ -z "$cidr" ] && { log_err "Usage: xdp-defense blocker del <ip/cidr>"; exit 1; }
|
||||||
|
|
||||||
if [[ "$cidr" == *":"* ]]; then
|
if [[ "$cidr" == *":"* ]]; then
|
||||||
@@ -321,7 +331,7 @@ cmd_blocker_del() {
|
|||||||
[ -z "$map_id" ] && { log_err "IPv6 map not found"; exit 1; }
|
[ -z "$map_id" ] && { log_err "IPv6 map not found"; exit 1; }
|
||||||
|
|
||||||
local key_hex
|
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; }
|
[ -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"
|
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 ====================
|
# ==================== Whitelist Commands ====================
|
||||||
|
|
||||||
cmd_whitelist_add() {
|
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; }
|
[ -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
|
# Direct IP/CIDR: contains a dot (IPv4) or colon (IPv6) with digits
|
||||||
@@ -450,10 +461,10 @@ cmd_whitelist_add() {
|
|||||||
local map_name key_hex
|
local map_name key_hex
|
||||||
if [[ "$name" == *":"* ]]; then
|
if [[ "$name" == *":"* ]]; then
|
||||||
map_name="whitelist_v6"
|
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
|
else
|
||||||
map_name="whitelist_v4"
|
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
|
fi
|
||||||
[ -z "$key_hex" ] && { log_err "Invalid CIDR: $name"; exit 1; }
|
[ -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; }
|
[ -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
|
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"
|
log_ok "Whitelisted: $name"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
@@ -470,7 +487,8 @@ cmd_whitelist_add() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd_whitelist_del() {
|
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; }
|
[ -z "$name" ] && { log_err "Usage: xdp-defense whitelist del <name|ip/cidr>"; exit 1; }
|
||||||
|
|
||||||
# Direct IP/CIDR
|
# Direct IP/CIDR
|
||||||
@@ -478,10 +496,10 @@ cmd_whitelist_del() {
|
|||||||
local map_name key_hex
|
local map_name key_hex
|
||||||
if [[ "$name" == *":"* ]]; then
|
if [[ "$name" == *":"* ]]; then
|
||||||
map_name="whitelist_v6"
|
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
|
else
|
||||||
map_name="whitelist_v4"
|
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
|
fi
|
||||||
[ -z "$key_hex" ] && { log_err "Invalid CIDR: $name"; exit 1; }
|
[ -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; }
|
[ -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"
|
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
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -516,10 +543,12 @@ for i, label in enumerate(labels):
|
|||||||
|
|
||||||
cmd_ddos_top() {
|
cmd_ddos_top() {
|
||||||
local n=${1:-10}
|
local n=${1:-10}
|
||||||
|
[[ "$n" =~ ^[0-9]+$ ]] || n=10
|
||||||
echo -e "${BOLD}=== Top $n IPs by Packet Count ===${NC}"
|
echo -e "${BOLD}=== Top $n IPs by Packet Count ===${NC}"
|
||||||
python3 -c "
|
python3 -c "
|
||||||
|
import sys
|
||||||
from ${COMMON_PY} import dump_rate_counters
|
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:
|
if not entries:
|
||||||
print(' (empty)')
|
print(' (empty)')
|
||||||
else:
|
else:
|
||||||
@@ -528,13 +557,13 @@ else:
|
|||||||
for ip, pkts, bts, _ in entries:
|
for ip, pkts, bts, _ in entries:
|
||||||
print(f' {ip:>18} {pkts:>10} {bts:>12}')
|
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:
|
if entries6:
|
||||||
print()
|
print()
|
||||||
print(' IPv6:')
|
print(' IPv6:')
|
||||||
for ip, pkts, bts, _ in entries6:
|
for ip, pkts, bts, _ in entries6:
|
||||||
print(f' {ip:>42} {pkts:>10} {bts:>12}')
|
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() {
|
cmd_ddos_blocked() {
|
||||||
@@ -569,11 +598,13 @@ else:
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd_ddos_block() {
|
cmd_ddos_block() {
|
||||||
local ip=$1
|
local ip
|
||||||
|
ip=$(sanitize_input "$1") || exit 1
|
||||||
local duration=${2:-0}
|
local duration=${2:-0}
|
||||||
[ -z "$ip" ] && { log_err "Usage: xdp-defense ddos block <ip> [duration_sec]"; exit 1; }
|
[ -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; }
|
{ log_err "Failed to block $ip"; exit 1; }
|
||||||
|
|
||||||
if [ "$duration" -gt 0 ] 2>/dev/null; then
|
if [ "$duration" -gt 0 ] 2>/dev/null; then
|
||||||
@@ -584,10 +615,11 @@ cmd_ddos_block() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd_ddos_unblock() {
|
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; }
|
[ -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_err "Failed to unblock $ip"; exit 1; }
|
||||||
log_ok "Unblocked $ip"
|
log_ok "Unblocked $ip"
|
||||||
}
|
}
|
||||||
@@ -637,25 +669,24 @@ if profiles:
|
|||||||
local key=$2
|
local key=$2
|
||||||
local value=$3
|
local value=$3
|
||||||
[ -z "$key" ] || [ -z "$value" ] && { log_err "Usage: xdp-defense ddos config set <pps|bps|window> <value>"; exit 1; }
|
[ -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 "
|
python3 -c "
|
||||||
|
import sys
|
||||||
from ${COMMON_PY} import read_rate_config, write_rate_config
|
from ${COMMON_PY} import read_rate_config, write_rate_config
|
||||||
cfg = read_rate_config()
|
cfg = read_rate_config()
|
||||||
if not cfg:
|
if not cfg:
|
||||||
cfg = {'pps_threshold': 1000, 'bps_threshold': 0, 'window_ns': 1000000000}
|
cfg = {'pps_threshold': 1000, 'bps_threshold': 0, 'window_ns': 1000000000}
|
||||||
key = '$key'
|
key, value = sys.argv[1], int(sys.argv[2])
|
||||||
value = int('$value')
|
|
||||||
if key == 'pps':
|
if key == 'pps':
|
||||||
cfg['pps_threshold'] = value
|
cfg['pps_threshold'] = value
|
||||||
elif key == 'bps':
|
elif key == 'bps':
|
||||||
cfg['bps_threshold'] = value
|
cfg['bps_threshold'] = value
|
||||||
elif key == 'window':
|
elif key == 'window':
|
||||||
cfg['window_ns'] = value * 1000000000
|
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'])
|
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"
|
log_ok "Set $key=$value"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
@@ -773,7 +804,8 @@ cmd_ai_retrain() {
|
|||||||
# ==================== GeoIP ====================
|
# ==================== GeoIP ====================
|
||||||
|
|
||||||
cmd_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; }
|
[ -z "$ip" ] && { log_err "Usage: xdp-defense geoip <ip>"; exit 1; }
|
||||||
|
|
||||||
echo "=== GeoIP Lookup: $ip ==="
|
echo "=== GeoIP Lookup: $ip ==="
|
||||||
@@ -829,7 +861,7 @@ cmd_geoip() {
|
|||||||
ddos_map_id=$(get_map_id blocked_ips_v4)
|
ddos_map_id=$(get_map_id blocked_ips_v4)
|
||||||
if [ -n "$ddos_map_id" ]; then
|
if [ -n "$ddos_map_id" ]; then
|
||||||
local ddos_key_hex
|
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
|
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}"
|
echo -e "DDoS: ${RED}BLOCKED${NC}"
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ struct traffic_features {
|
|||||||
__u64 udp_count;
|
__u64 udp_count;
|
||||||
__u64 icmp_count;
|
__u64 icmp_count;
|
||||||
__u64 other_proto_count;
|
__u64 other_proto_count;
|
||||||
__u64 unique_ips_approx; // approximate via counter
|
__u64 unique_ips_approx; // per-packet counter (not truly unique, used as relative indicator)
|
||||||
__u64 small_pkt_count; // packets < 100 bytes
|
__u64 small_pkt_count; // packets < 100 bytes
|
||||||
__u64 large_pkt_count; // packets > 1400 bytes
|
__u64 large_pkt_count; // packets > 1400 bytes
|
||||||
};
|
};
|
||||||
@@ -218,6 +218,9 @@ static __always_inline int check_blocked_v6(struct in6_addr *ip, __u64 now) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Rate check for IPv4: returns 1 if rate exceeded
|
// Rate check for IPv4: returns 1 if rate exceeded
|
||||||
|
// Note: LRU_HASH lookups on multi-CPU are racy (no per-entry lock), so counters
|
||||||
|
// may be slightly inaccurate under high concurrency. This is acceptable for rate
|
||||||
|
// limiting where approximate enforcement is sufficient.
|
||||||
static __always_inline int rate_check_v4(__u32 ip, __u64 now, __u64 pkt_len) {
|
static __always_inline int rate_check_v4(__u32 ip, __u64 now, __u64 pkt_len) {
|
||||||
__u32 cfg_key = 0;
|
__u32 cfg_key = 0;
|
||||||
struct rate_cfg *cfg = bpf_map_lookup_elem(&rate_config, &cfg_key);
|
struct rate_cfg *cfg = bpf_map_lookup_elem(&rate_config, &cfg_key);
|
||||||
@@ -349,6 +352,12 @@ int xdp_ddos(struct xdp_md *ctx) {
|
|||||||
__u8 proto = iph->protocol;
|
__u8 proto = iph->protocol;
|
||||||
__u8 tcp_flags = 0;
|
__u8 tcp_flags = 0;
|
||||||
|
|
||||||
|
// Validate IHL (minimum 5 = 20 bytes)
|
||||||
|
if (iph->ihl < 5) {
|
||||||
|
inc_stat(4);
|
||||||
|
return XDP_PASS;
|
||||||
|
}
|
||||||
|
|
||||||
// Extract TCP flags if applicable
|
// Extract TCP flags if applicable
|
||||||
if (proto == IPPROTO_TCP) {
|
if (proto == IPPROTO_TCP) {
|
||||||
struct tcphdr *tcph = l3_hdr + (iph->ihl * 4);
|
struct tcphdr *tcph = l3_hdr + (iph->ihl * 4);
|
||||||
@@ -393,9 +402,35 @@ int xdp_ddos(struct xdp_md *ctx) {
|
|||||||
struct in6_addr saddr = ip6h->saddr;
|
struct in6_addr saddr = ip6h->saddr;
|
||||||
__u8 proto = ip6h->nexthdr;
|
__u8 proto = ip6h->nexthdr;
|
||||||
__u8 tcp_flags = 0;
|
__u8 tcp_flags = 0;
|
||||||
|
void *next_hdr = (void *)(ip6h + 1);
|
||||||
|
|
||||||
|
// Skip known IPv6 extension headers (up to 4 to stay within verifier limits)
|
||||||
|
#pragma unroll
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
if (proto != IPPROTO_HOPOPTS && proto != IPPROTO_ROUTING &&
|
||||||
|
proto != IPPROTO_DSTOPTS && proto != IPPROTO_FRAGMENT)
|
||||||
|
break;
|
||||||
|
if (proto == IPPROTO_FRAGMENT) {
|
||||||
|
// Fragment header is fixed 8 bytes
|
||||||
|
if (next_hdr + 8 > data_end)
|
||||||
|
break;
|
||||||
|
proto = *(__u8 *)next_hdr;
|
||||||
|
next_hdr += 8;
|
||||||
|
} else {
|
||||||
|
// Other extension headers: length in 2nd byte (units of 8 octets, +8)
|
||||||
|
if (next_hdr + 2 > data_end)
|
||||||
|
break;
|
||||||
|
__u8 ext_len = *((__u8 *)next_hdr + 1);
|
||||||
|
__u32 hdr_len = (((__u32)ext_len) + 1) * 8;
|
||||||
|
if (next_hdr + hdr_len > data_end)
|
||||||
|
break;
|
||||||
|
proto = *(__u8 *)next_hdr;
|
||||||
|
next_hdr += hdr_len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (proto == IPPROTO_TCP) {
|
if (proto == IPPROTO_TCP) {
|
||||||
struct tcphdr *tcph = (void *)(ip6h + 1);
|
struct tcphdr *tcph = next_hdr;
|
||||||
if ((void *)(tcph + 1) <= data_end) {
|
if ((void *)(tcph + 1) <= data_end) {
|
||||||
tcp_flags = ((__u8 *)tcph)[13];
|
tcp_flags = ((__u8 *)tcph)[13];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ RestartSec=5
|
|||||||
|
|
||||||
# Security hardening
|
# Security hardening
|
||||||
ProtectSystem=strict
|
ProtectSystem=strict
|
||||||
ReadWritePaths=/var/lib/xdp-defense /etc/xdp-defense /etc/xdp-blocker /sys/fs/bpf
|
ReadWritePaths=/var/lib/xdp-defense /etc/xdp-defense /etc/xdp-blocker /sys/fs/bpf /tmp
|
||||||
ProtectHome=true
|
ProtectHome=true
|
||||||
NoNewPrivileges=false
|
NoNewPrivileges=false
|
||||||
CapabilityBoundingSet=CAP_NET_ADMIN CAP_BPF CAP_SYS_ADMIN CAP_PERFMON
|
CapabilityBoundingSet=CAP_NET_ADMIN CAP_BPF CAP_SYS_ADMIN CAP_PERFMON
|
||||||
|
|||||||
@@ -32,8 +32,18 @@ if _syslog_handler:
|
|||||||
|
|
||||||
# ==================== BPF Map Helpers ====================
|
# ==================== BPF Map Helpers ====================
|
||||||
|
|
||||||
|
_map_cache = {} # {map_name: (map_id, timestamp)}
|
||||||
|
_MAP_CACHE_TTL = 5.0 # seconds
|
||||||
|
|
||||||
def get_map_id(map_name):
|
def get_map_id(map_name):
|
||||||
"""Get BPF map ID by name."""
|
"""Get BPF map ID by name (cached with 5s TTL)."""
|
||||||
|
import time as _time
|
||||||
|
now = _time.monotonic()
|
||||||
|
|
||||||
|
cached = _map_cache.get(map_name)
|
||||||
|
if cached and (now - cached[1]) < _MAP_CACHE_TTL:
|
||||||
|
return cached[0]
|
||||||
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["bpftool", "map", "show", "-j"],
|
["bpftool", "map", "show", "-j"],
|
||||||
capture_output=True, text=True
|
capture_output=True, text=True
|
||||||
@@ -42,12 +52,15 @@ def get_map_id(map_name):
|
|||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
maps = json.loads(result.stdout)
|
maps = json.loads(result.stdout)
|
||||||
|
# Update cache for all maps found
|
||||||
for m in maps:
|
for m in maps:
|
||||||
if m.get("name") == map_name:
|
name = m.get("name")
|
||||||
return m.get("id")
|
if name:
|
||||||
|
_map_cache[name] = (m.get("id"), now)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return None
|
cached = _map_cache.get(map_name)
|
||||||
|
return cached[0] if cached else None
|
||||||
|
|
||||||
|
|
||||||
# ==================== CIDR / IPv4 Helpers (from blocker) ====================
|
# ==================== CIDR / IPv4 Helpers (from blocker) ====================
|
||||||
@@ -130,10 +143,12 @@ def batch_map_operation(map_id, cidrs, operation="update", value_hex="01 00 00 0
|
|||||||
"""
|
"""
|
||||||
total = len(cidrs)
|
total = len(cidrs)
|
||||||
processed = 0
|
processed = 0
|
||||||
|
written = 0
|
||||||
|
|
||||||
for i in range(0, total, batch_size):
|
for i in range(0, total, batch_size):
|
||||||
batch = cidrs[i:i + batch_size]
|
batch = cidrs[i:i + batch_size]
|
||||||
batch_file = None
|
batch_file = None
|
||||||
|
batch_written = 0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.batch', delete=False) as f:
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.batch', delete=False) as f:
|
||||||
@@ -145,6 +160,7 @@ def batch_map_operation(map_id, cidrs, operation="update", value_hex="01 00 00 0
|
|||||||
f.write(f"map update id {map_id} key hex {key_hex} value hex {value_hex}\n")
|
f.write(f"map update id {map_id} key hex {key_hex} value hex {value_hex}\n")
|
||||||
else:
|
else:
|
||||||
f.write(f"map delete id {map_id} key hex {key_hex}\n")
|
f.write(f"map delete id {map_id} key hex {key_hex}\n")
|
||||||
|
batch_written += 1
|
||||||
except (ValueError, Exception):
|
except (ValueError, Exception):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -157,13 +173,44 @@ def batch_map_operation(map_id, cidrs, operation="update", value_hex="01 00 00 0
|
|||||||
os.unlink(batch_file)
|
os.unlink(batch_file)
|
||||||
|
|
||||||
processed += len(batch)
|
processed += len(batch)
|
||||||
|
written += batch_written
|
||||||
pct = processed * 100 // total if total > 0 else 100
|
pct = processed * 100 // total if total > 0 else 100
|
||||||
print(f"\r Progress: {processed}/{total} ({pct}%)", end="", flush=True)
|
print(f"\r Progress: {processed}/{total} ({pct}%)", end="", flush=True)
|
||||||
|
|
||||||
print()
|
print()
|
||||||
proto = "v6" if ipv6 else "v4"
|
proto = "v6" if ipv6 else "v4"
|
||||||
audit_log.info(f"batch {operation} {proto}: {processed} entries on map {map_id}")
|
audit_log.info(f"batch {operation} {proto}: {written} entries on map {map_id}")
|
||||||
return processed
|
return written
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== Whitelist Check (for daemon) ====================
|
||||||
|
|
||||||
|
def is_whitelisted(ip_str):
|
||||||
|
"""Check if an IP is in the BPF whitelist maps.
|
||||||
|
Returns True if whitelisted, False otherwise.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
addr = ipaddress.ip_address(ip_str)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if isinstance(addr, ipaddress.IPv6Address):
|
||||||
|
map_name = "whitelist_v6"
|
||||||
|
key_hex = cidr_to_key_v6(f"{ip_str}/128")
|
||||||
|
else:
|
||||||
|
map_name = "whitelist_v4"
|
||||||
|
key_hex = cidr_to_key(f"{ip_str}/32")
|
||||||
|
|
||||||
|
map_id = get_map_id(map_name)
|
||||||
|
if map_id is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
["bpftool", "map", "lookup", "id", str(map_id),
|
||||||
|
"key", "hex"] + key_hex.split(),
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
return result.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
# ==================== IP Encoding Helpers (from ddos) ====================
|
# ==================== IP Encoding Helpers (from ddos) ====================
|
||||||
@@ -360,16 +407,17 @@ def block_ip(ip_str, duration_sec=0):
|
|||||||
|
|
||||||
key_hex = ip_to_hex_key(ip_str)
|
key_hex = ip_to_hex_key(ip_str)
|
||||||
|
|
||||||
|
# Use CLOCK_BOOTTIME (matches BPF ktime_get_ns)
|
||||||
|
with open('/proc/uptime', 'r') as f:
|
||||||
|
uptime_sec = float(f.read().split()[0])
|
||||||
|
now_ns = int(uptime_sec * 1_000_000_000)
|
||||||
|
|
||||||
if duration_sec > 0:
|
if duration_sec > 0:
|
||||||
with open('/proc/uptime', 'r') as f:
|
|
||||||
uptime_sec = float(f.read().split()[0])
|
|
||||||
now_ns = int(uptime_sec * 1_000_000_000)
|
|
||||||
expire_ns = now_ns + (duration_sec * 1_000_000_000)
|
expire_ns = now_ns + (duration_sec * 1_000_000_000)
|
||||||
else:
|
else:
|
||||||
expire_ns = 0
|
expire_ns = 0
|
||||||
|
|
||||||
now_ns_val = 0
|
raw = struct.pack('<QQQ', expire_ns, now_ns, 0)
|
||||||
raw = struct.pack('<QQQ', expire_ns, now_ns_val, 0)
|
|
||||||
val_hex = ' '.join(f"{b:02x}" for b in raw)
|
val_hex = ' '.join(f"{b:02x}" for b in raw)
|
||||||
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
|
|||||||
@@ -133,7 +133,8 @@ def list_countries():
|
|||||||
|
|
||||||
for cc_file in sorted(files):
|
for cc_file in sorted(files):
|
||||||
cc = cc_file.stem.upper()
|
cc = cc_file.stem.upper()
|
||||||
count = sum(1 for _ in open(cc_file))
|
with open(cc_file) as f:
|
||||||
|
count = sum(1 for _ in f)
|
||||||
mtime = cc_file.stat().st_mtime
|
mtime = cc_file.stat().st_mtime
|
||||||
age = int((time.time() - mtime) / 86400)
|
age = int((time.time() - mtime) / 86400)
|
||||||
print(f" {cc}: {count} CIDRs (updated {age}d ago)")
|
print(f" {cc}: {count} CIDRs (updated {age}d ago)")
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ time-profile switching, and automatic escalation.
|
|||||||
- Cleanup Thread: removes expired entries from blocked_ips maps
|
- Cleanup Thread: removes expired entries from blocked_ips maps
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import copy
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
@@ -57,12 +58,10 @@ DEFAULT_CONFIG = {
|
|||||||
'profiles': {},
|
'profiles': {},
|
||||||
},
|
},
|
||||||
'escalation': {
|
'escalation': {
|
||||||
'rate_limit_after': 1,
|
|
||||||
'temp_block_after': 5,
|
'temp_block_after': 5,
|
||||||
'perm_block_after': 20,
|
'perm_block_after': 20,
|
||||||
'temp_block_duration': 300,
|
'temp_block_duration': 300,
|
||||||
'violation_window': 600,
|
'violation_window': 600,
|
||||||
'cooldown_multiplier': 0.5,
|
|
||||||
},
|
},
|
||||||
'ewma': {
|
'ewma': {
|
||||||
'alpha': 0.3,
|
'alpha': 0.3,
|
||||||
@@ -78,7 +77,6 @@ DEFAULT_CONFIG = {
|
|||||||
'min_samples': 1000,
|
'min_samples': 1000,
|
||||||
'poll_interval': 5,
|
'poll_interval': 5,
|
||||||
'anomaly_threshold': -0.3,
|
'anomaly_threshold': -0.3,
|
||||||
'retrain_interval': 604800,
|
|
||||||
'min_packets_for_sample': 20,
|
'min_packets_for_sample': 20,
|
||||||
'model_file': '/var/lib/xdp-defense/ai_model.pkl',
|
'model_file': '/var/lib/xdp-defense/ai_model.pkl',
|
||||||
'training_data_file': '/var/lib/xdp-defense/training_data.csv',
|
'training_data_file': '/var/lib/xdp-defense/training_data.csv',
|
||||||
@@ -90,7 +88,7 @@ CONFIG_PATH = '/etc/xdp-defense/config.yaml'
|
|||||||
|
|
||||||
def load_config(path=CONFIG_PATH):
|
def load_config(path=CONFIG_PATH):
|
||||||
"""Load config with defaults."""
|
"""Load config with defaults."""
|
||||||
cfg = DEFAULT_CONFIG.copy()
|
cfg = copy.deepcopy(DEFAULT_CONFIG)
|
||||||
try:
|
try:
|
||||||
with open(path) as f:
|
with open(path) as f:
|
||||||
user = yaml.safe_load(f) or {}
|
user = yaml.safe_load(f) or {}
|
||||||
@@ -406,8 +404,9 @@ class DDoSDaemon:
|
|||||||
def __init__(self, config_path=CONFIG_PATH):
|
def __init__(self, config_path=CONFIG_PATH):
|
||||||
self.config_path = config_path
|
self.config_path = config_path
|
||||||
self.cfg = load_config(config_path)
|
self.cfg = load_config(config_path)
|
||||||
self.running = False
|
|
||||||
self._stop_event = threading.Event()
|
self._stop_event = threading.Event()
|
||||||
|
self._ewma_interval = self.cfg['ewma'].get('poll_interval', 1)
|
||||||
|
self._ai_interval = self.cfg['ai'].get('poll_interval', 5)
|
||||||
self._setup_components()
|
self._setup_components()
|
||||||
|
|
||||||
def _setup_components(self):
|
def _setup_components(self):
|
||||||
@@ -465,12 +464,21 @@ class DDoSDaemon:
|
|||||||
def _handle_sighup(self, signum, frame):
|
def _handle_sighup(self, signum, frame):
|
||||||
log.info("SIGHUP received, reloading config...")
|
log.info("SIGHUP received, reloading config...")
|
||||||
self.cfg = load_config(self.config_path)
|
self.cfg = load_config(self.config_path)
|
||||||
self._setup_components()
|
# Update existing components without rebuilding (preserves EWMA/violation state)
|
||||||
log.info("Config reloaded")
|
self.violation_tracker.cfg = self.cfg['escalation']
|
||||||
|
self.ewma_analyzer.alpha = self.cfg['ewma'].get('alpha', 0.3)
|
||||||
|
self.ewma_analyzer.threshold_multiplier = self.cfg['ewma'].get('threshold_multiplier', 3.0)
|
||||||
|
self.ai_detector.cfg = self.cfg['ai']
|
||||||
|
self.profile_manager.cfg = self.cfg['rate_limits']
|
||||||
|
# Update poll intervals (used by threads on next iteration)
|
||||||
|
self._ewma_interval = self.cfg['ewma'].get('poll_interval', 1)
|
||||||
|
self._ai_interval = self.cfg['ai'].get('poll_interval', 5)
|
||||||
|
level = self.cfg['general'].get('log_level', 'info').upper()
|
||||||
|
log.setLevel(getattr(logging, level, logging.INFO))
|
||||||
|
log.info("Config reloaded (state preserved)")
|
||||||
|
|
||||||
def _handle_sigterm(self, signum, frame):
|
def _handle_sigterm(self, signum, frame):
|
||||||
log.info("SIGTERM received, shutting down...")
|
log.info("SIGTERM received, shutting down...")
|
||||||
self.running = False
|
|
||||||
self._stop_event.set()
|
self._stop_event.set()
|
||||||
|
|
||||||
def _handle_sigusr1(self, signum, frame):
|
def _handle_sigusr1(self, signum, frame):
|
||||||
@@ -481,12 +489,12 @@ class DDoSDaemon:
|
|||||||
|
|
||||||
def _ewma_thread(self):
|
def _ewma_thread(self):
|
||||||
"""Poll rate counters, compute EWMA, detect violations, escalate."""
|
"""Poll rate counters, compute EWMA, detect violations, escalate."""
|
||||||
from xdp_common import dump_rate_counters, block_ip
|
from xdp_common import dump_rate_counters, block_ip, is_whitelisted
|
||||||
|
|
||||||
interval = self.cfg['ewma'].get('poll_interval', 1)
|
|
||||||
prev_counters = {}
|
prev_counters = {}
|
||||||
|
|
||||||
while self.running:
|
while not self._stop_event.is_set():
|
||||||
|
interval = self._ewma_interval
|
||||||
try:
|
try:
|
||||||
entries = dump_rate_counters('rate_counter_v4', top_n=1000)
|
entries = dump_rate_counters('rate_counter_v4', top_n=1000)
|
||||||
active_ips = []
|
active_ips = []
|
||||||
@@ -505,6 +513,11 @@ class DDoSDaemon:
|
|||||||
|
|
||||||
is_anomalous = self.ewma_analyzer.update(ip_str, pps)
|
is_anomalous = self.ewma_analyzer.update(ip_str, pps)
|
||||||
if is_anomalous:
|
if is_anomalous:
|
||||||
|
# Skip whitelisted IPs
|
||||||
|
if is_whitelisted(ip_str):
|
||||||
|
log.debug("EWMA anomaly skipped (whitelisted): %s", ip_str)
|
||||||
|
continue
|
||||||
|
|
||||||
level = self.violation_tracker.record_violation(ip_str)
|
level = self.violation_tracker.record_violation(ip_str)
|
||||||
ew = self.ewma_analyzer.get_stats(ip_str)
|
ew = self.ewma_analyzer.get_stats(ip_str)
|
||||||
log.warning(
|
log.warning(
|
||||||
@@ -536,12 +549,12 @@ class DDoSDaemon:
|
|||||||
|
|
||||||
def _ai_thread(self):
|
def _ai_thread(self):
|
||||||
"""Read traffic features, run AI inference or collect training data."""
|
"""Read traffic features, run AI inference or collect training data."""
|
||||||
from xdp_common import read_percpu_features, dump_rate_counters, block_ip
|
from xdp_common import read_percpu_features, dump_rate_counters, block_ip, is_whitelisted
|
||||||
|
|
||||||
interval = self.cfg['ai'].get('poll_interval', 5)
|
|
||||||
prev_features = None
|
prev_features = None
|
||||||
|
|
||||||
while self.running:
|
while not self._stop_event.is_set():
|
||||||
|
interval = self._ai_interval
|
||||||
try:
|
try:
|
||||||
if not self.ai_detector.enabled:
|
if not self.ai_detector.enabled:
|
||||||
self._stop_event.wait(interval)
|
self._stop_event.wait(interval)
|
||||||
@@ -593,6 +606,11 @@ class DDoSDaemon:
|
|||||||
)
|
)
|
||||||
top_ips = dump_rate_counters('rate_counter_v4', top_n=5)
|
top_ips = dump_rate_counters('rate_counter_v4', top_n=5)
|
||||||
for ip_str, pkts, bts, _ in top_ips:
|
for ip_str, pkts, bts, _ in top_ips:
|
||||||
|
# Skip whitelisted IPs
|
||||||
|
if is_whitelisted(ip_str):
|
||||||
|
log.debug("AI escalation skipped (whitelisted): %s", ip_str)
|
||||||
|
continue
|
||||||
|
|
||||||
level = self.violation_tracker.record_violation(ip_str)
|
level = self.violation_tracker.record_violation(ip_str)
|
||||||
log.warning("AI escalation: %s -> %s", ip_str, level)
|
log.warning("AI escalation: %s -> %s", ip_str, level)
|
||||||
|
|
||||||
@@ -620,7 +638,7 @@ class DDoSDaemon:
|
|||||||
|
|
||||||
def _profile_thread(self):
|
def _profile_thread(self):
|
||||||
"""Check time-of-day and switch rate profiles."""
|
"""Check time-of-day and switch rate profiles."""
|
||||||
while self.running:
|
while not self._stop_event.is_set():
|
||||||
try:
|
try:
|
||||||
self.profile_manager.check_and_apply()
|
self.profile_manager.check_and_apply()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -631,7 +649,7 @@ class DDoSDaemon:
|
|||||||
"""Periodically clean up expired blocked IPs and stale violations."""
|
"""Periodically clean up expired blocked IPs and stale violations."""
|
||||||
from xdp_common import dump_blocked_ips, unblock_ip
|
from xdp_common import dump_blocked_ips, unblock_ip
|
||||||
|
|
||||||
while self.running:
|
while not self._stop_event.is_set():
|
||||||
try:
|
try:
|
||||||
with open('/proc/uptime') as f:
|
with open('/proc/uptime') as f:
|
||||||
now_ns = int(float(f.read().split()[0]) * 1_000_000_000)
|
now_ns = int(float(f.read().split()[0]) * 1_000_000_000)
|
||||||
@@ -667,7 +685,6 @@ class DDoSDaemon:
|
|||||||
|
|
||||||
self._ensure_single_instance()
|
self._ensure_single_instance()
|
||||||
self._write_pid()
|
self._write_pid()
|
||||||
self.running = True
|
|
||||||
|
|
||||||
threads = [
|
threads = [
|
||||||
threading.Thread(target=self._ewma_thread, name='ewma', daemon=True),
|
threading.Thread(target=self._ewma_thread, name='ewma', daemon=True),
|
||||||
@@ -683,13 +700,12 @@ class DDoSDaemon:
|
|||||||
log.info("Daemon running (PID %d)", os.getpid())
|
log.info("Daemon running (PID %d)", os.getpid())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while self.running:
|
while not self._stop_event.is_set():
|
||||||
self._stop_event.wait(1)
|
self._stop_event.wait(1)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
log.info("Shutting down...")
|
log.info("Shutting down...")
|
||||||
self.running = False
|
|
||||||
self._stop_event.set()
|
self._stop_event.set()
|
||||||
|
|
||||||
for t in threads:
|
for t in threads:
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ PRESETS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def download_cloudflare():
|
def download_cloudflare():
|
||||||
"""Download Cloudflare IP ranges"""
|
"""Download Cloudflare IP ranges (IPv4 + IPv6)"""
|
||||||
cidrs = []
|
cidrs = []
|
||||||
try:
|
try:
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
@@ -43,10 +43,23 @@ def download_cloudflare():
|
|||||||
headers={"User-Agent": "xdp-whitelist/1.0"}
|
headers={"User-Agent": "xdp-whitelist/1.0"}
|
||||||
)
|
)
|
||||||
with urllib.request.urlopen(req) as r:
|
with urllib.request.urlopen(req) as r:
|
||||||
cidrs.extend(r.read().decode().strip().split('\n'))
|
v4 = r.read().decode().strip().split('\n')
|
||||||
print(f" Downloaded {len(cidrs)} IPv4 ranges")
|
cidrs.extend(v4)
|
||||||
|
print(f" Downloaded {len(v4)} IPv4 ranges")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" [WARN] Failed to download IPv4: {e}")
|
print(f" [WARN] Failed to download IPv4: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(
|
||||||
|
PRESETS["cloudflare"]["v6"],
|
||||||
|
headers={"User-Agent": "xdp-whitelist/1.0"}
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req) as r:
|
||||||
|
v6 = r.read().decode().strip().split('\n')
|
||||||
|
cidrs.extend(v6)
|
||||||
|
print(f" Downloaded {len(v6)} IPv6 ranges")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [WARN] Failed to download IPv6: {e}")
|
||||||
return cidrs
|
return cidrs
|
||||||
|
|
||||||
def download_aws():
|
def download_aws():
|
||||||
@@ -103,7 +116,7 @@ def add_whitelist(name, cidrs=None):
|
|||||||
|
|
||||||
if cidrs is None and wl_file.exists():
|
if cidrs is None and wl_file.exists():
|
||||||
with open(wl_file) as f:
|
with open(wl_file) as f:
|
||||||
cidrs = [line.strip() for line in f if line.strip() and ':' not in line]
|
cidrs = [line.strip() for line in f if line.strip()]
|
||||||
if cidrs:
|
if cidrs:
|
||||||
print(f"[INFO] Using cached {name} ({len(cidrs)} CIDRs)")
|
print(f"[INFO] Using cached {name} ({len(cidrs)} CIDRs)")
|
||||||
|
|
||||||
@@ -200,9 +213,15 @@ def list_whitelist():
|
|||||||
|
|
||||||
for wl_file in sorted(files):
|
for wl_file in sorted(files):
|
||||||
name = wl_file.stem
|
name = wl_file.stem
|
||||||
count = sum(1 for line in open(wl_file) if line.strip() and ':' not in line)
|
with open(wl_file) as f:
|
||||||
|
cidrs = [line.strip() for line in f if line.strip()]
|
||||||
|
v4_count = sum(1 for c in cidrs if ':' not in c)
|
||||||
|
v6_count = len(cidrs) - v4_count
|
||||||
desc = PRESETS.get(name, {}).get("desc", "Custom")
|
desc = PRESETS.get(name, {}).get("desc", "Custom")
|
||||||
print(f" {name}: {count} CIDRs ({desc})")
|
if v6_count > 0:
|
||||||
|
print(f" {name}: {v4_count} v4 + {v6_count} v6 CIDRs ({desc})")
|
||||||
|
else:
|
||||||
|
print(f" {name}: {v4_count} CIDRs ({desc})")
|
||||||
|
|
||||||
def show_presets():
|
def show_presets():
|
||||||
"""Show available presets"""
|
"""Show available presets"""
|
||||||
|
|||||||
Reference in New Issue
Block a user