Add CDN filter and fix xdp-cdn-update bugs
- Add xdp_cdn_filter BPF program (priority 5) to allow only CDN/whitelist on port 80/443 - Fix \r carriage return bug preventing BunnyCDN IPv4 loading (594 IPs were silently failing) - Fix BPF map flush code to handle list-type keys from bpftool JSON output - Fix per-cpu stats parsing to use formatted values from bpftool - Replace in-loop counter with post-load BPF map verification for accurate counts - Remove xdp_cdn_load.py (consolidated into xdp-cdn-update) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
5
Makefile
5
Makefile
@@ -17,7 +17,7 @@ CLANG ?= clang
|
||||
CLANG_FLAGS := -O2 -g -Wall -target bpf \
|
||||
-I/usr/include -I/usr/include/bpf -I/usr/include/xdp
|
||||
|
||||
BPF_OBJECTS := $(BPF_DIR)/xdp_blocker.o $(BPF_DIR)/xdp_ddos.o
|
||||
BPF_OBJECTS := $(BPF_DIR)/xdp_cdn_filter.o $(BPF_DIR)/xdp_blocker.o $(BPF_DIR)/xdp_ddos.o
|
||||
|
||||
.PHONY: all build install uninstall enable disable clean check-deps status
|
||||
|
||||
@@ -100,3 +100,6 @@ check-deps:
|
||||
@python3 -c "import yaml" 2>/dev/null || (echo "ERROR: python3-yaml not found" && exit 1)
|
||||
@test -f /usr/include/xdp/xdp_helpers.h || (echo "ERROR: xdp_helpers.h not found" && exit 1)
|
||||
@echo "All dependencies satisfied"
|
||||
|
||||
$(BPF_DIR)/xdp_cdn_filter.o: $(BPF_DIR)/xdp_cdn_filter.c
|
||||
$(CLANG) $(CLANG_FLAGS) -c $< -o $@
|
||||
|
||||
222
bin/xdp-cdn-update
Executable file
222
bin/xdp-cdn-update
Executable file
@@ -0,0 +1,222 @@
|
||||
#!/bin/bash
|
||||
# XDP CDN Filter - IP 맵 업데이트
|
||||
# BunnyNet + Cloudflare IP를 cdn_allow_v4/v6 BPF 맵에 로드
|
||||
# Usage: xdp-cdn-update [--force]
|
||||
set -euo pipefail
|
||||
|
||||
CDN_IPS_FILE="/opt/haproxy/conf/cdn-ips.lst"
|
||||
STATE_FILE="/var/lib/xdp-defense/cdn-ips.hash"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_ok() { echo -e "${GREEN}[OK]${NC} $1"; }
|
||||
log_err() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
|
||||
log_info() { echo -e "${CYAN}[INFO]${NC} $1"; }
|
||||
|
||||
get_map_id() {
|
||||
bpftool map show -j 2>/dev/null | python3 -c "
|
||||
import sys, json
|
||||
maps = json.load(sys.stdin)
|
||||
for m in maps:
|
||||
if m.get('name') == '$1':
|
||||
print(m['id'])
|
||||
break
|
||||
" 2>/dev/null
|
||||
}
|
||||
|
||||
# Fetch fresh CDN IPs if the file doesn't exist or --force
|
||||
fetch_ips() {
|
||||
local tmp="${CDN_IPS_FILE}.tmp"
|
||||
echo "# CDN IP allowlist - updated $(date -u '+%Y-%m-%d %H:%M UTC')" > "$tmp"
|
||||
|
||||
echo "# BunnyNet IPv4" >> "$tmp"
|
||||
curl -sf --max-time 10 "https://api.bunny.net/system/edgeserverlist/plain" | tr -d '\r' >> "$tmp" 2>/dev/null || true
|
||||
echo "" >> "$tmp"
|
||||
|
||||
echo "# BunnyNet IPv6" >> "$tmp"
|
||||
curl -sf --max-time 10 "https://bunnycdn.com/api/system/edgeserverlist/IPv6" | tr -d '[]"\r' | tr ',' '\n' | sed 's/^ *//' >> "$tmp" 2>/dev/null || true
|
||||
echo "" >> "$tmp"
|
||||
|
||||
echo "# Cloudflare IPv4" >> "$tmp"
|
||||
curl -sf --max-time 10 "https://www.cloudflare.com/ips-v4/" | tr -d '\r' >> "$tmp" 2>/dev/null || true
|
||||
echo "" >> "$tmp"
|
||||
|
||||
echo "# Cloudflare IPv6" >> "$tmp"
|
||||
curl -sf --max-time 10 "https://www.cloudflare.com/ips-v6/" | tr -d '\r' >> "$tmp" 2>/dev/null || true
|
||||
echo "" >> "$tmp"
|
||||
|
||||
local total
|
||||
total=$(grep -cvE '^(#|$)' "$tmp" || echo 0)
|
||||
if [ "$total" -lt 50 ]; then
|
||||
log_err "Only $total IPs fetched, keeping old file"
|
||||
rm -f "$tmp"
|
||||
return 1
|
||||
fi
|
||||
|
||||
mv "$tmp" "$CDN_IPS_FILE"
|
||||
log_info "Fetched $total IPs"
|
||||
}
|
||||
|
||||
# Load IPs into BPF maps
|
||||
load_maps() {
|
||||
local v4_id v6_id
|
||||
v4_id=$(get_map_id "cdn_allow_v4")
|
||||
v6_id=$(get_map_id "cdn_allow_v6")
|
||||
|
||||
if [ -z "$v4_id" ] || [ -z "$v6_id" ]; then
|
||||
log_err "CDN BPF maps not found. Is xdp_cdn_filter loaded?"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Flush existing entries
|
||||
python3 -c "
|
||||
import sys, json, subprocess
|
||||
def flush_map(map_id):
|
||||
dump = subprocess.run(['bpftool','map','dump','id', str(map_id), '-j'],
|
||||
capture_output=True, text=True)
|
||||
if dump.returncode != 0:
|
||||
return 0
|
||||
entries = json.loads(dump.stdout)
|
||||
count = 0
|
||||
for e in entries:
|
||||
key = e.get('key', [])
|
||||
if isinstance(key, list):
|
||||
hex_bytes = [k.replace('0x','') for k in key]
|
||||
cmd = ['bpftool','map','delete','id', str(map_id),'key','hex'] + hex_bytes
|
||||
if subprocess.run(cmd, capture_output=True).returncode == 0:
|
||||
count += 1
|
||||
return count
|
||||
flush_map($v4_id)
|
||||
flush_map($v6_id)
|
||||
" 2>/dev/null || true
|
||||
|
||||
# Load IPs from file and count via BPF map
|
||||
while IFS= read -r line; do
|
||||
# Skip comments and empty lines
|
||||
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
||||
[[ -z "${line// /}" ]] && continue
|
||||
line="${line%%#*}" # strip inline comments
|
||||
line="${line// /}" # strip whitespace
|
||||
|
||||
if [[ "$line" == *":"* ]]; then
|
||||
# IPv6
|
||||
local addr="$line"
|
||||
local prefix=128
|
||||
if [[ "$line" == *"/"* ]]; then
|
||||
addr="${line%/*}"
|
||||
prefix="${line#*/}"
|
||||
fi
|
||||
local hex_key
|
||||
hex_key=$(python3 -c "
|
||||
import socket, struct
|
||||
addr = socket.inet_pton(socket.AF_INET6, '$addr')
|
||||
prefix = $prefix
|
||||
key = struct.pack('<I', prefix) + addr
|
||||
print(' '.join(f'{b:02x}' for b in key))
|
||||
" 2>/dev/null) || continue
|
||||
bpftool map update id "$v6_id" key hex $hex_key value hex 01 2>/dev/null || true
|
||||
else
|
||||
# IPv4
|
||||
local addr="$line"
|
||||
local prefix=32
|
||||
if [[ "$line" == *"/"* ]]; then
|
||||
addr="${line%/*}"
|
||||
prefix="${line#*/}"
|
||||
fi
|
||||
local hex_key
|
||||
hex_key=$(python3 -c "
|
||||
import socket, struct
|
||||
addr = socket.inet_aton('$addr')
|
||||
prefix = $prefix
|
||||
key = struct.pack('<I', prefix) + addr
|
||||
print(' '.join(f'{b:02x}' for b in key))
|
||||
" 2>/dev/null) || continue
|
||||
bpftool map update id "$v4_id" key hex $hex_key value hex 01 2>/dev/null || true
|
||||
fi
|
||||
done < "$CDN_IPS_FILE"
|
||||
|
||||
# Verify actual counts from BPF maps
|
||||
local v4_count v6_count
|
||||
v4_count=$(bpftool map dump id "$v4_id" -j 2>/dev/null | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo 0)
|
||||
v6_count=$(bpftool map dump id "$v6_id" -j 2>/dev/null | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo 0)
|
||||
|
||||
log_ok "Loaded: IPv4=$v4_count, IPv6=$v6_count"
|
||||
}
|
||||
|
||||
# Stats
|
||||
show_stats() {
|
||||
local stats_id
|
||||
stats_id=$(get_map_id "cdn_stats")
|
||||
if [ -z "$stats_id" ]; then
|
||||
log_err "cdn_stats map not found"
|
||||
return 1
|
||||
fi
|
||||
echo "=== XDP CDN Filter Stats ==="
|
||||
bpftool map dump id "$stats_id" -j 2>/dev/null | python3 -c "
|
||||
import sys, json
|
||||
labels = ['CDN pass', 'Whitelist pass', 'Non-web pass', 'Dropped']
|
||||
try:
|
||||
entries = json.load(sys.stdin)
|
||||
for e in entries:
|
||||
# Use formatted section if available (bpftool with BTF)
|
||||
fmt = e.get('formatted', {})
|
||||
if fmt:
|
||||
idx = int(fmt['key'])
|
||||
total = sum(v['value'] for v in fmt['values'])
|
||||
else:
|
||||
key = e.get('key', 0)
|
||||
if isinstance(key, list):
|
||||
idx = int(key[0], 16) if isinstance(key[0], str) else int(key[0])
|
||||
else:
|
||||
idx = int(key)
|
||||
vals = e.get('values', e.get('value', []))
|
||||
total = 0
|
||||
if isinstance(vals, list):
|
||||
for v in vals:
|
||||
if isinstance(v, dict):
|
||||
total += int(v.get('value', v.get('val', 0)))
|
||||
else:
|
||||
total += int(v)
|
||||
else:
|
||||
total = int(vals)
|
||||
if idx < len(labels):
|
||||
print(f' {labels[idx]:<20} {total:>12}')
|
||||
except Exception as ex:
|
||||
print(f' Parse error: {ex}', file=sys.stderr)
|
||||
"
|
||||
}
|
||||
|
||||
main() {
|
||||
case "${1:-update}" in
|
||||
update|--force)
|
||||
if [ ! -f "$CDN_IPS_FILE" ] || [ "${1:-}" = "--force" ]; then
|
||||
fetch_ips
|
||||
fi
|
||||
# Check if hash changed
|
||||
local new_hash
|
||||
new_hash=$(md5sum "$CDN_IPS_FILE" | cut -d' ' -f1)
|
||||
local old_hash=""
|
||||
[ -f "$STATE_FILE" ] && old_hash=$(cat "$STATE_FILE")
|
||||
if [ "$new_hash" = "$old_hash" ] && [ "${1:-}" != "--force" ]; then
|
||||
log_info "No changes (hash: $new_hash)"
|
||||
return 0
|
||||
fi
|
||||
load_maps
|
||||
echo "$new_hash" > "$STATE_FILE"
|
||||
;;
|
||||
fetch)
|
||||
fetch_ips
|
||||
;;
|
||||
stats)
|
||||
show_stats
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 [update|fetch|stats|--force]"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -117,6 +117,10 @@ cmd_load() {
|
||||
xdp-loader load "$iface" "$BPF_DIR/xdp_blocker.o" \
|
||||
--pin-path "$PIN_PATH"
|
||||
|
||||
log_info "Loading XDP CDN filter (priority 5) on $iface..."
|
||||
xdp-loader load "$iface" "$BPF_DIR/xdp_cdn_filter.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"
|
||||
@@ -127,7 +131,7 @@ cmd_load() {
|
||||
# Apply rate config
|
||||
cmd_ddos_config_apply quiet
|
||||
|
||||
log_ok "XDP Defense loaded on $iface (2 programs chained)"
|
||||
log_ok "XDP Defense loaded on $iface (3 programs chained)"
|
||||
|
||||
# Restore blocklist
|
||||
[ -f "$BLOCKLIST_FILE" ] && cmd_blocker_restore || true
|
||||
@@ -153,6 +157,14 @@ cmd_load() {
|
||||
python3 "$LIB_DIR/xdp_whitelist.py" add "$name" >/dev/null 2>&1 || true
|
||||
done
|
||||
fi
|
||||
|
||||
# Restore CDN allowlist
|
||||
if [ -f "/opt/haproxy/conf/cdn-ips.lst" ]; then
|
||||
log_info "Restoring CDN IP allowlist..."
|
||||
python3 /opt/xdp-defense/bin/xdp_cdn_load.py 2>&1 | while read -r line; do
|
||||
log_info " $line"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_unload() {
|
||||
|
||||
228
bpf/xdp_cdn_filter.c
Normal file
228
bpf/xdp_cdn_filter.c
Normal file
@@ -0,0 +1,228 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
// XDP CDN Filter - Port 80/443 트래픽을 CDN IP + 화이트리스트 IP만 허용
|
||||
// Part of xdp-defense: chained via libxdp dispatcher (priority 5)
|
||||
#include <linux/bpf.h>
|
||||
#include <linux/if_ether.h>
|
||||
#include <linux/ip.h>
|
||||
#include <linux/ipv6.h>
|
||||
#include <linux/tcp.h>
|
||||
#include <linux/udp.h>
|
||||
#include <linux/in.h>
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_endian.h>
|
||||
#include <xdp/xdp_helpers.h>
|
||||
|
||||
// LPM trie keys (must match xdp_blocker.c exactly)
|
||||
struct ipv4_lpm_key {
|
||||
__u32 prefixlen;
|
||||
__u32 addr;
|
||||
};
|
||||
|
||||
struct ipv6_lpm_key {
|
||||
__u32 prefixlen;
|
||||
__u8 addr[16];
|
||||
};
|
||||
|
||||
// VLAN header (802.1Q)
|
||||
struct vlan_hdr {
|
||||
__be16 h_vlan_TCI;
|
||||
__be16 h_vlan_encapsulated_proto;
|
||||
};
|
||||
|
||||
// Shared whitelist maps (pinned by xdp_blocker, reused here)
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_LPM_TRIE);
|
||||
__type(key, struct ipv4_lpm_key);
|
||||
__type(value, __u64);
|
||||
__uint(max_entries, 4096);
|
||||
__uint(map_flags, BPF_F_NO_PREALLOC);
|
||||
__uint(pinning, LIBBPF_PIN_BY_NAME);
|
||||
} whitelist_v4 SEC(".maps");
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_LPM_TRIE);
|
||||
__type(key, struct ipv6_lpm_key);
|
||||
__type(value, __u64);
|
||||
__uint(max_entries, 4096);
|
||||
__uint(map_flags, BPF_F_NO_PREALLOC);
|
||||
__uint(pinning, LIBBPF_PIN_BY_NAME);
|
||||
} whitelist_v6 SEC(".maps");
|
||||
|
||||
// CDN IPv4 allowlist (BunnyNet + Cloudflare)
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_LPM_TRIE);
|
||||
__type(key, struct ipv4_lpm_key);
|
||||
__type(value, __u8);
|
||||
__uint(max_entries, 4096);
|
||||
__uint(map_flags, BPF_F_NO_PREALLOC);
|
||||
} cdn_allow_v4 SEC(".maps");
|
||||
|
||||
// CDN IPv6 allowlist (BunnyNet + Cloudflare)
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_LPM_TRIE);
|
||||
__type(key, struct ipv6_lpm_key);
|
||||
__type(value, __u8);
|
||||
__uint(max_entries, 1024);
|
||||
__uint(map_flags, BPF_F_NO_PREALLOC);
|
||||
} cdn_allow_v6 SEC(".maps");
|
||||
|
||||
// Per-CPU stats: 0=passed(cdn), 1=passed(whitelist), 2=passed(non-web), 3=dropped
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
|
||||
__type(key, __u32);
|
||||
__type(value, __u64);
|
||||
__uint(max_entries, 4);
|
||||
} cdn_stats SEC(".maps");
|
||||
|
||||
static __always_inline void inc_stat(__u32 idx) {
|
||||
__u64 *val = bpf_map_lookup_elem(&cdn_stats, &idx);
|
||||
if (val)
|
||||
(*val)++;
|
||||
}
|
||||
|
||||
// Extract dest port from transport header. Returns 0 if not TCP/UDP.
|
||||
static __always_inline __u16 get_dport(void *transport, void *data_end, __u8 proto) {
|
||||
if (proto == IPPROTO_TCP) {
|
||||
struct tcphdr *tcp = transport;
|
||||
if ((void *)(tcp + 1) > data_end)
|
||||
return 0;
|
||||
return bpf_ntohs(tcp->dest);
|
||||
}
|
||||
if (proto == IPPROTO_UDP) {
|
||||
struct udphdr *udp = transport;
|
||||
if ((void *)(udp + 1) > data_end)
|
||||
return 0;
|
||||
return bpf_ntohs(udp->dest);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
SEC("xdp")
|
||||
int xdp_cdn_filter(struct xdp_md *ctx) {
|
||||
void *data = (void *)(long)ctx->data;
|
||||
void *data_end = (void *)(long)ctx->data_end;
|
||||
|
||||
struct ethhdr *eth = data;
|
||||
if ((void *)(eth + 1) > data_end)
|
||||
return XDP_PASS;
|
||||
|
||||
__u16 eth_proto = bpf_ntohs(eth->h_proto);
|
||||
void *l3_hdr = (void *)(eth + 1);
|
||||
|
||||
// Handle VLAN tags
|
||||
if (eth_proto == ETH_P_8021Q || eth_proto == ETH_P_8021AD) {
|
||||
struct vlan_hdr *vhdr = l3_hdr;
|
||||
if ((void *)(vhdr + 1) > data_end)
|
||||
return XDP_PASS;
|
||||
eth_proto = bpf_ntohs(vhdr->h_vlan_encapsulated_proto);
|
||||
l3_hdr = (void *)(vhdr + 1);
|
||||
if (eth_proto == ETH_P_8021Q || eth_proto == ETH_P_8021AD) {
|
||||
vhdr = l3_hdr;
|
||||
if ((void *)(vhdr + 1) > data_end)
|
||||
return XDP_PASS;
|
||||
eth_proto = bpf_ntohs(vhdr->h_vlan_encapsulated_proto);
|
||||
l3_hdr = (void *)(vhdr + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// === IPv4 ===
|
||||
if (eth_proto == ETH_P_IP) {
|
||||
struct iphdr *iph = l3_hdr;
|
||||
if ((void *)(iph + 1) > data_end)
|
||||
return XDP_PASS;
|
||||
|
||||
if (iph->ihl < 5)
|
||||
return XDP_PASS;
|
||||
|
||||
__u8 proto = iph->protocol;
|
||||
void *transport = (void *)iph + (iph->ihl * 4);
|
||||
__u16 dport = get_dport(transport, data_end, proto);
|
||||
|
||||
// Not port 80/443 → pass (don't interfere with SSH, Tailscale, etc.)
|
||||
if (dport != 80 && dport != 443) {
|
||||
inc_stat(2);
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
struct ipv4_lpm_key key = { .prefixlen = 32, .addr = iph->saddr };
|
||||
|
||||
// Check shared whitelist (internal/Tailscale IPs)
|
||||
if (bpf_map_lookup_elem(&whitelist_v4, &key)) {
|
||||
inc_stat(1);
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
// Check CDN allowlist
|
||||
if (bpf_map_lookup_elem(&cdn_allow_v4, &key)) {
|
||||
inc_stat(0);
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
// Not CDN, not whitelisted → DROP
|
||||
inc_stat(3);
|
||||
return XDP_DROP;
|
||||
}
|
||||
|
||||
// === IPv6 ===
|
||||
if (eth_proto == ETH_P_IPV6) {
|
||||
struct ipv6hdr *ip6h = l3_hdr;
|
||||
if ((void *)(ip6h + 1) > data_end)
|
||||
return XDP_PASS;
|
||||
|
||||
__u8 proto = ip6h->nexthdr;
|
||||
void *transport = (void *)(ip6h + 1);
|
||||
|
||||
// Skip extension headers (up to 4)
|
||||
#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) {
|
||||
if (transport + 8 > data_end) break;
|
||||
proto = *(__u8 *)transport;
|
||||
transport += 8;
|
||||
} else {
|
||||
if (transport + 2 > data_end) break;
|
||||
__u8 ext_len = *((__u8 *)transport + 1);
|
||||
__u32 hdr_len = (((__u32)ext_len) + 1) * 8;
|
||||
if (transport + hdr_len > data_end) break;
|
||||
proto = *(__u8 *)transport;
|
||||
transport += hdr_len;
|
||||
}
|
||||
}
|
||||
|
||||
__u16 dport = get_dport(transport, data_end, proto);
|
||||
|
||||
if (dport != 80 && dport != 443) {
|
||||
inc_stat(2);
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
struct ipv6_lpm_key key = { .prefixlen = 128 };
|
||||
__builtin_memcpy(key.addr, &ip6h->saddr, 16);
|
||||
|
||||
if (bpf_map_lookup_elem(&whitelist_v6, &key)) {
|
||||
inc_stat(1);
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
if (bpf_map_lookup_elem(&cdn_allow_v6, &key)) {
|
||||
inc_stat(0);
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
inc_stat(3);
|
||||
return XDP_DROP;
|
||||
}
|
||||
|
||||
return XDP_PASS; // ARP etc.
|
||||
}
|
||||
|
||||
char _license[] SEC("license") = "GPL";
|
||||
|
||||
// libxdp dispatcher: priority 5, chain on XDP_PASS
|
||||
struct {
|
||||
__uint(priority, 5);
|
||||
__uint(XDP_PASS, 1);
|
||||
} XDP_RUN_CONFIG(xdp_cdn_filter);
|
||||
@@ -631,6 +631,9 @@ class DDoSDaemon:
|
||||
self._last_retrain_time = self._get_model_mtime()
|
||||
self._last_log_cleanup = time.time()
|
||||
|
||||
self._blocked_ips = set()
|
||||
self._blocked_ips_lock = threading.Lock()
|
||||
|
||||
self._init_traffic_db()
|
||||
|
||||
level = self.cfg['general'].get('log_level', 'info').upper()
|
||||
@@ -830,19 +833,33 @@ class DDoSDaemon:
|
||||
)
|
||||
|
||||
if level == 'temp_block':
|
||||
dur = self.cfg['escalation'].get('temp_block_duration', 300)
|
||||
try:
|
||||
block_ip(ip_str, dur)
|
||||
log.warning("TEMP BLOCK: %s for %ds", ip_str, dur)
|
||||
except Exception as e:
|
||||
log.error("Failed to temp-block %s: %s", ip_str, e)
|
||||
with self._blocked_ips_lock:
|
||||
already = ip_str in self._blocked_ips
|
||||
if already:
|
||||
log.debug("EWMA skip (already blocked): %s", ip_str)
|
||||
else:
|
||||
dur = self.cfg['escalation'].get('temp_block_duration', 300)
|
||||
try:
|
||||
block_ip(ip_str, dur)
|
||||
with self._blocked_ips_lock:
|
||||
self._blocked_ips.add(ip_str)
|
||||
log.warning("TEMP BLOCK: %s for %ds", ip_str, dur)
|
||||
except Exception as e:
|
||||
log.error("Failed to temp-block %s: %s", ip_str, e)
|
||||
|
||||
elif level == 'perm_block':
|
||||
try:
|
||||
block_ip(ip_str, 0)
|
||||
log.warning("PERM BLOCK: %s", ip_str)
|
||||
except Exception as e:
|
||||
log.error("Failed to perm-block %s: %s", ip_str, e)
|
||||
with self._blocked_ips_lock:
|
||||
already = ip_str in self._blocked_ips
|
||||
if already:
|
||||
log.debug("EWMA skip (already blocked): %s", ip_str)
|
||||
else:
|
||||
try:
|
||||
block_ip(ip_str, 0)
|
||||
with self._blocked_ips_lock:
|
||||
self._blocked_ips.add(ip_str)
|
||||
log.warning("PERM BLOCK: %s", ip_str)
|
||||
except Exception as e:
|
||||
log.error("Failed to perm-block %s: %s", ip_str, e)
|
||||
|
||||
self.ewma_analyzer.cleanup_stale(active_ips)
|
||||
|
||||
@@ -970,19 +987,33 @@ class DDoSDaemon:
|
||||
log.warning("AI escalation: %s ewma=%.1f baseline=%.1f -> %s", ip_str, ewma, baseline, level)
|
||||
|
||||
if level == 'temp_block':
|
||||
dur = self.cfg['escalation'].get('temp_block_duration', 300)
|
||||
try:
|
||||
block_ip(ip_str, dur)
|
||||
log.warning("AI TEMP BLOCK: %s for %ds", ip_str, dur)
|
||||
except Exception as e:
|
||||
log.error("Failed to AI temp-block %s: %s", ip_str, e)
|
||||
with self._blocked_ips_lock:
|
||||
already = ip_str in self._blocked_ips
|
||||
if already:
|
||||
log.debug("AI skip (already blocked): %s", ip_str)
|
||||
else:
|
||||
dur = self.cfg['escalation'].get('temp_block_duration', 300)
|
||||
try:
|
||||
block_ip(ip_str, dur)
|
||||
with self._blocked_ips_lock:
|
||||
self._blocked_ips.add(ip_str)
|
||||
log.warning("AI TEMP BLOCK: %s for %ds", ip_str, dur)
|
||||
except Exception as e:
|
||||
log.error("Failed to AI temp-block %s: %s", ip_str, e)
|
||||
|
||||
elif level == 'perm_block':
|
||||
try:
|
||||
block_ip(ip_str, 0)
|
||||
log.warning("AI PERM BLOCK: %s", ip_str)
|
||||
except Exception as e:
|
||||
log.error("Failed to AI perm-block %s: %s", ip_str, e)
|
||||
with self._blocked_ips_lock:
|
||||
already = ip_str in self._blocked_ips
|
||||
if already:
|
||||
log.debug("AI skip (already blocked): %s", ip_str)
|
||||
else:
|
||||
try:
|
||||
block_ip(ip_str, 0)
|
||||
with self._blocked_ips_lock:
|
||||
self._blocked_ips.add(ip_str)
|
||||
log.warning("AI PERM BLOCK: %s", ip_str)
|
||||
except Exception as e:
|
||||
log.error("Failed to AI perm-block %s: %s", ip_str, e)
|
||||
|
||||
prev_features = features
|
||||
|
||||
@@ -1014,6 +1045,8 @@ class DDoSDaemon:
|
||||
try:
|
||||
unblock_ip(ip_str)
|
||||
self.violation_tracker.clear(ip_str)
|
||||
with self._blocked_ips_lock:
|
||||
self._blocked_ips.discard(ip_str)
|
||||
log.info("Expired block removed: %s (dropped %d pkts)", ip_str, drop_count)
|
||||
except Exception as e:
|
||||
log.error("Failed to remove expired block %s: %s", ip_str, e)
|
||||
|
||||
Reference in New Issue
Block a user