From 2c29eab991bd7f11cf26318d70494ad4e42a6e6e Mon Sep 17 00:00:00 2001 From: kaffa Date: Sat, 7 Feb 2026 11:22:55 +0900 Subject: [PATCH] Fix HIGH severity security and thread-safety issues Daemon fixes: - Add _db_lock for thread-safe SQLite access - Atomic SIGHUP config swap (build all values before applying) - Check world-writable permission before loading pickle model - Write model files with 0o600 permissions via os.open - Module-level xdp_common import with fatal exit on failure - Close traffic DB on shutdown - Add period_data parameter to _train() to avoid race condition CLI fixes: - Replace $COMMON_PY variable with hardcoded 'xdp_common' - Pass CONFIG_FILE via sys.argv instead of string interpolation - Add key_hex regex validation before all bpftool commands - Switch sanitize_input from denylist to strict allowlist Co-Authored-By: Claude Opus 4.6 --- bin/xdp-defense | 88 ++++++++++++++------------- lib/xdp_defense_daemon.py | 123 ++++++++++++++++++++++++-------------- 2 files changed, 125 insertions(+), 86 deletions(-) diff --git a/bin/xdp-defense b/bin/xdp-defense index 6e97594..134f9b0 100755 --- a/bin/xdp-defense +++ b/bin/xdp-defense @@ -14,7 +14,6 @@ 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:-}" @@ -30,10 +29,11 @@ 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 - strict allowlist for IP addresses, CIDR notation, preset names sanitize_input() { local val="$1" - if [[ "$val" =~ [\'\"\;\$\`\\] ]]; then + # Allow only alphanumeric, dots, colons, slashes, hyphens, underscores + if [[ ! "$val" =~ ^[a-zA-Z0-9._:/\ -]+$ ]]; then log_err "Invalid characters in input: $val" return 1 fi @@ -44,10 +44,10 @@ get_iface() { if [ -f "$CONFIG_FILE" ]; then local iface iface=$(python3 -c " -import yaml -with open('$CONFIG_FILE') as f: +import yaml, sys +with open(sys.argv[1]) as f: print(yaml.safe_load(f).get('general',{}).get('interface','eth0')) -" 2>/dev/null) +" "$CONFIG_FILE" 2>/dev/null) echo "${iface:-eth0}" else echo "eth0" @@ -249,7 +249,7 @@ cmd_status() { # Rate config python3 -c " -from ${COMMON_PY} import read_rate_config +from xdp_common 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)') @@ -259,7 +259,7 @@ else: # Blocked IPs python3 -c " -from ${COMMON_PY} import dump_blocked_ips +from xdp_common 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)}') @@ -283,8 +283,9 @@ cmd_blocker_add() { [ -z "$map_id" ] && { log_err "IPv6 map not found. Is XDP loaded?"; exit 1; } local key_hex - 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) + key_hex=$(python3 -c "import sys; from xdp_common 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; } + [[ "$key_hex" =~ ^[0-9a-f\ ]+$ ]] || { log_err "Invalid key hex"; 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 @@ -312,6 +313,7 @@ cmd_blocker_add() { 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") + [[ "$key_hex" =~ ^[0-9a-f\ ]+$ ]] || { log_err "Invalid key hex"; 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: $cidr" || true @@ -331,8 +333,9 @@ cmd_blocker_del() { [ -z "$map_id" ] && { log_err "IPv6 map not found"; exit 1; } local key_hex - 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) + key_hex=$(python3 -c "import sys; from xdp_common 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; } + [[ "$key_hex" =~ ^[0-9a-f\ ]+$ ]] || { log_err "Invalid key hex"; 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.$$" @@ -358,6 +361,7 @@ cmd_blocker_del() { 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") + [[ "$key_hex" =~ ^[0-9a-f\ ]+$ ]] || { log_err "Invalid key hex"; exit 1; } bpftool map delete id "$map_id" key hex $key_hex 2>/dev/null && log_ok "Removed: $cidr" local tmpfile="${BLOCKLIST_FILE}.tmp.$$" @@ -461,12 +465,13 @@ cmd_whitelist_add() { local map_name key_hex if [[ "$name" == *":"* ]]; then map_name="whitelist_v6" - 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) + key_hex=$(python3 -c "import sys; from xdp_common 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 "import sys; from ${COMMON_PY} import cidr_to_key; print(cidr_to_key(sys.argv[1]))" "$name" 2>/dev/null) + key_hex=$(python3 -c "import sys; from xdp_common 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; } + [[ "$key_hex" =~ ^[0-9a-f\ ]+$ ]] || { log_err "Invalid key hex"; exit 1; } local map_id map_id=$(get_map_id "$map_name") @@ -496,12 +501,13 @@ cmd_whitelist_del() { local map_name key_hex if [[ "$name" == *":"* ]]; then map_name="whitelist_v6" - 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) + key_hex=$(python3 -c "import sys; from xdp_common 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 "import sys; from ${COMMON_PY} import cidr_to_key; print(cidr_to_key(sys.argv[1]))" "$name" 2>/dev/null) + key_hex=$(python3 -c "import sys; from xdp_common 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; } + [[ "$key_hex" =~ ^[0-9a-f\ ]+$ ]] || { log_err "Invalid key hex"; exit 1; } local map_id map_id=$(get_map_id "$map_name") @@ -532,7 +538,7 @@ cmd_whitelist_list() { cmd_ddos_stats() { echo -e "${BOLD}=== DDoS Statistics ===${NC}" python3 -c " -from ${COMMON_PY} import read_percpu_stats +from xdp_common 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): @@ -547,7 +553,7 @@ cmd_ddos_top() { echo -e "${BOLD}=== Top $n IPs by Packet Count ===${NC}" python3 -c " import sys -from ${COMMON_PY} import dump_rate_counters +from xdp_common import dump_rate_counters entries = dump_rate_counters('rate_counter_v4', int(sys.argv[1])) if not entries: print(' (empty)') @@ -569,7 +575,7 @@ if entries6: cmd_ddos_blocked() { echo -e "${BOLD}=== Blocked IPs ===${NC}" python3 -c " -from ${COMMON_PY} import dump_blocked_ips +from xdp_common import dump_blocked_ips with open('/proc/uptime') as f: now_ns = int(float(f.read().split()[0]) * 1_000_000_000) @@ -604,7 +610,7 @@ cmd_ddos_block() { [ -z "$ip" ] && { log_err "Usage: xdp-defense ddos block [duration_sec]"; exit 1; } [[ "$duration" =~ ^[0-9]+$ ]] || { log_err "Invalid duration: $duration"; exit 1; } - python3 -c "import sys; from ${COMMON_PY} import block_ip; block_ip(sys.argv[1], int(sys.argv[2]))" "$ip" "$duration" 2>/dev/null || \ + python3 -c "import sys; from xdp_common 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 @@ -619,7 +625,7 @@ cmd_ddos_unblock() { ip=$(sanitize_input "$1") || exit 1 [ -z "$ip" ] && { log_err "Usage: xdp-defense ddos unblock "; exit 1; } - python3 -c "import sys; from ${COMMON_PY} import unblock_ip; unblock_ip(sys.argv[1])" "$ip" 2>/dev/null || \ + python3 -c "import sys; from xdp_common import unblock_ip; unblock_ip(sys.argv[1])" "$ip" 2>/dev/null || \ { log_err "Failed to unblock $ip"; exit 1; } log_ok "Unblocked $ip" } @@ -631,7 +637,7 @@ cmd_ddos_config() { echo -e "${BOLD}=== Rate Configuration ===${NC}" echo -e "\n${CYAN}Active (BPF map):${NC}" python3 -c " -from ${COMMON_PY} import read_rate_config +from xdp_common import read_rate_config cfg = read_rate_config() if cfg: pps = cfg['pps_threshold'] @@ -647,8 +653,8 @@ else: if [ -f "$CONFIG_FILE" ]; then echo -e "\n${CYAN}Config file ($CONFIG_FILE):${NC}" python3 -c " -import yaml -with open('$CONFIG_FILE') as f: +import yaml, sys +with open(sys.argv[1]) as f: cfg = yaml.safe_load(f) rl = cfg.get('rate_limits', {}) print(f' Default PPS: {rl.get(\"default_pps\", \"N/A\")}') @@ -662,7 +668,7 @@ if profiles: hours = p.get('hours', '') pps = p.get('pps', 'N/A') print(f' {name}: pps={pps}, hours={hours}') -" 2>/dev/null +" "$CONFIG_FILE" 2>/dev/null fi ;; set) @@ -674,7 +680,7 @@ if profiles: python3 -c " import sys -from ${COMMON_PY} import read_rate_config, write_rate_config +from xdp_common import read_rate_config, write_rate_config cfg = read_rate_config() if not cfg: cfg = {'pps_threshold': 1000, 'bps_threshold': 0, 'window_ns': 1000000000} @@ -700,16 +706,16 @@ cmd_ddos_config_apply() { [ ! -f "$CONFIG_FILE" ] && return python3 -c " -import yaml -from ${COMMON_PY} import write_rate_config -with open('$CONFIG_FILE') as f: +import yaml, sys +from xdp_common import write_rate_config +with open(sys.argv[1]) 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 +" "$CONFIG_FILE" 2>/dev/null || return 0 [ "$quiet" != "quiet" ] && log_ok "Config applied from $CONFIG_FILE" || true } @@ -777,8 +783,8 @@ cmd_ai_status() { if [ -f "$CONFIG_FILE" ]; then python3 -c " -import yaml -with open('$CONFIG_FILE') as f: +import yaml, sys +with open(sys.argv[1]) as f: cfg = yaml.safe_load(f) ai = cfg.get('ai', {}) enabled = ai.get('enabled', False) @@ -786,7 +792,7 @@ if enabled: print(f'AI Detection: enabled ({ai.get(\"model_type\", \"IsolationForest\")})') else: print('AI Detection: disabled') -" 2>/dev/null +" "$CONFIG_FILE" 2>/dev/null fi } @@ -804,11 +810,11 @@ cmd_ai_retrain() { cmd_ai_traffic() { local db_file db_file=$(python3 -c " -import yaml -with open('$CONFIG_FILE') as f: +import yaml, sys +with open(sys.argv[1]) as f: cfg = yaml.safe_load(f) print(cfg.get('ai',{}).get('traffic_log_db', '/var/lib/xdp-defense/traffic_log.db')) -" 2>/dev/null || echo "/var/lib/xdp-defense/traffic_log.db") +" "$CONFIG_FILE" 2>/dev/null || echo "/var/lib/xdp-defense/traffic_log.db") [ ! -f "$db_file" ] && { log_err "Traffic log not found: $db_file"; exit 1; } @@ -872,7 +878,7 @@ conn.close() # Show next retrain time import yaml, os, time try: - with open('$CONFIG_FILE') as f: + with open(sys.argv[2]) as f: cfg = yaml.safe_load(f) retrain_interval = cfg.get('ai',{}).get('retrain_interval', 86400) model_file = cfg.get('ai',{}).get('model_file', '/var/lib/xdp-defense/ai_model.pkl') @@ -890,7 +896,7 @@ try: except: pass print() -" "$db_file" +" "$db_file" "$CONFIG_FILE" } cmd_ai_log() { @@ -899,11 +905,11 @@ cmd_ai_log() { local db_file db_file=$(python3 -c " -import yaml -with open('$CONFIG_FILE') as f: +import yaml, sys +with open(sys.argv[1]) as f: cfg = yaml.safe_load(f) print(cfg.get('ai',{}).get('traffic_log_db', '/var/lib/xdp-defense/traffic_log.db')) -" 2>/dev/null || echo "/var/lib/xdp-defense/traffic_log.db") +" "$CONFIG_FILE" 2>/dev/null || echo "/var/lib/xdp-defense/traffic_log.db") [ ! -f "$db_file" ] && { log_err "Traffic log not found: $db_file"; exit 1; } @@ -1001,6 +1007,7 @@ cmd_geoip() { 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") + [[ "$key_hex" =~ ^[0-9a-f\ ]+$ ]] || { log_err "Invalid key hex"; exit 1; } 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 @@ -1013,7 +1020,8 @@ 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 "import sys; from ${COMMON_PY} import ip_to_hex_key; print(ip_to_hex_key(sys.argv[1]))" "$ip" 2>/dev/null) + ddos_key_hex=$(python3 -c "import sys; from xdp_common import ip_to_hex_key; print(ip_to_hex_key(sys.argv[1]))" "$ip" 2>/dev/null) + [[ "$ddos_key_hex" =~ ^[0-9a-f\ ]+$ ]] || { log_err "Invalid key hex"; exit 1; } 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 diff --git a/lib/xdp_defense_daemon.py b/lib/xdp_defense_daemon.py index 3159a11..90d08c7 100755 --- a/lib/xdp_defense_daemon.py +++ b/lib/xdp_defense_daemon.py @@ -14,6 +14,7 @@ time-profile switching, and automatic escalation. import copy import math import os +import stat import sys import time import signal @@ -28,6 +29,16 @@ from datetime import datetime, timedelta import yaml +try: + from xdp_common import ( + dump_rate_counters, block_ip, is_whitelisted, + read_percpu_features, dump_blocked_ips, unblock_ip, + write_rate_config, read_rate_config, + ) +except ImportError as e: + print(f"FATAL: Cannot import xdp_common: {e}", file=sys.stderr) + sys.exit(1) + # ==================== Logging ==================== log = logging.getLogger('xdp-defense-daemon') @@ -256,8 +267,13 @@ class AIDetector: self._train() self._retrain_requested = False - def _train(self): - """Train per-period Isolation Forest models.""" + def _train(self, period_data=None): + """Train per-period Isolation Forest models. + If period_data provided, use it instead of self.training_data. + """ + if period_data is None: + period_data = self.training_data + try: from sklearn.ensemble import IsolationForest from sklearn.preprocessing import StandardScaler @@ -267,19 +283,19 @@ class AIDetector: self.cfg['enabled'] = False return - total = sum(len(v) for v in self.training_data.values()) + total = sum(len(v) for v in period_data.values()) if total < 10: log.warning("Not enough training data (%d samples)", total) return log.info("Training AI models: %s", - {p: len(s) for p, s in self.training_data.items() if s}) + {p: len(s) for p, s in period_data.items() if s}) try: new_models = {} all_samples = [] - for period, samples in self.training_data.items(): + for period, samples in period_data.items(): if len(samples) < 10: log.info("Period %s: %d samples (too few, skip)", period, len(samples)) continue @@ -309,7 +325,8 @@ class AIDetector: # Save to disk (atomic) model_file = self.cfg.get('model_file', '/var/lib/xdp-defense/ai_model.pkl') tmp_model = model_file + '.tmp' - with open(tmp_model, 'wb') as f: + fd = os.open(tmp_model, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) + with os.fdopen(fd, 'wb') as f: pickle.dump({ 'format': 'period_models', 'models': {p: {'model': m['model'], 'scaler': m['scaler']} @@ -323,7 +340,8 @@ class AIDetector: # Save training data CSV data_file = self.cfg.get('training_data_file', '/var/lib/xdp-defense/training_data.csv') tmp_data = data_file + '.tmp' - with open(tmp_data, 'w', newline='') as f: + fd = os.open(tmp_data, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o644) + with os.fdopen(fd, 'w', newline='') as f: writer = csv.writer(f) writer.writerow([ 'hour_sin', 'hour_cos', @@ -348,6 +366,12 @@ class AIDetector: if not os.path.exists(model_file): return False try: + st = os.stat(model_file) + # Warn if file is world-writable + if st.st_mode & stat.S_IWOTH: + log.warning("Model file %s is world-writable! Refusing to load.", model_file) + return False + with open(model_file, 'rb') as f: data = pickle.load(f) @@ -488,8 +512,7 @@ class AIDetector: len(rows), filtered_count, {p: len(s) for p, s in period_data.items() if s}) - self.training_data = period_data - self._train() + self._train(period_data=period_data) return not self.is_learning @@ -504,8 +527,6 @@ class ProfileManager: def check_and_apply(self): """Check current time and apply matching profile.""" - from xdp_common import write_rate_config - profiles = self.cfg.get('profiles', {}) now = datetime.now() current_hour = now.hour @@ -648,17 +669,25 @@ class DDoSDaemon: def _handle_sighup(self, signum, frame): log.info("SIGHUP received, reloading config...") - self.cfg = load_config(self.config_path) - # Update existing components without rebuilding (preserves EWMA/violation state) - 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() + new_cfg = load_config(self.config_path) + # Build all new values before swapping anything + new_escalation = new_cfg['escalation'] + new_alpha = new_cfg['ewma'].get('alpha', 0.3) + new_threshold = new_cfg['ewma'].get('threshold_multiplier', 3.0) + new_ai_cfg = new_cfg['ai'] + new_rate_cfg = new_cfg['rate_limits'] + new_ewma_interval = new_cfg['ewma'].get('poll_interval', 1) + new_ai_interval = new_cfg['ai'].get('poll_interval', 5) + level = new_cfg['general'].get('log_level', 'info').upper() + # Now apply all at once + self.cfg = new_cfg + self.violation_tracker.cfg = new_escalation + self.ewma_analyzer.alpha = new_alpha + self.ewma_analyzer.threshold_multiplier = new_threshold + self.ai_detector.cfg = new_ai_cfg + self.profile_manager.cfg = new_rate_cfg + self._ewma_interval = new_ewma_interval + self._ai_interval = new_ai_interval log.setLevel(getattr(logging, level, logging.INFO)) log.info("Config reloaded (state preserved)") @@ -678,6 +707,7 @@ class DDoSDaemon: """Initialize SQLite database for traffic logging.""" db_path = self.cfg['ai'].get('traffic_log_db', '/var/lib/xdp-defense/traffic_log.db') os.makedirs(os.path.dirname(db_path), exist_ok=True) + self._db_lock = threading.Lock() self._traffic_db = sqlite3.connect(db_path, check_same_thread=False) self._traffic_db.execute( 'CREATE TABLE IF NOT EXISTS traffic_samples (' @@ -712,17 +742,18 @@ class DDoSDaemon: def _log_traffic(self, now, hour, features): """Insert one row into traffic_samples table.""" try: - self._traffic_db.execute( - 'INSERT INTO traffic_samples (' - ' timestamp, hour, hour_sin, hour_cos,' - ' total_packets, total_bytes, tcp_syn_count, tcp_other_count,' - ' udp_count, icmp_count, other_proto_count, unique_ips_approx,' - ' small_pkt_count, large_pkt_count,' - ' syn_ratio, udp_ratio, icmp_ratio, small_pkt_ratio, avg_pkt_size' - ') VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', - (now.isoformat(), hour, *features) - ) - self._traffic_db.commit() + with self._db_lock: + self._traffic_db.execute( + 'INSERT INTO traffic_samples (' + ' timestamp, hour, hour_sin, hour_cos,' + ' total_packets, total_bytes, tcp_syn_count, tcp_other_count,' + ' udp_count, icmp_count, other_proto_count, unique_ips_approx,' + ' small_pkt_count, large_pkt_count,' + ' syn_ratio, udp_ratio, icmp_ratio, small_pkt_ratio, avg_pkt_size' + ') VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', + (now.isoformat(), hour, *features) + ) + self._traffic_db.commit() except Exception as e: log.error("Failed to write traffic log: %s", e) @@ -732,13 +763,14 @@ class DDoSDaemon: cutoff = (datetime.now() - timedelta(days=retention_days)).isoformat() try: - cur = self._traffic_db.execute( - 'DELETE FROM traffic_samples WHERE timestamp < ?', (cutoff,) - ) - deleted = cur.rowcount - self._traffic_db.commit() - if deleted > 1000: - self._traffic_db.execute('VACUUM') + with self._db_lock: + cur = self._traffic_db.execute( + 'DELETE FROM traffic_samples WHERE timestamp < ?', (cutoff,) + ) + deleted = cur.rowcount + self._traffic_db.commit() + if deleted > 1000: + self._traffic_db.execute('VACUUM') log.info("Traffic log cleanup: deleted %d rows (retention=%dd)", deleted, retention_days) except Exception as e: log.error("Traffic log cleanup failed: %s", e) @@ -755,8 +787,6 @@ class DDoSDaemon: def _ewma_thread(self): """Poll rate counters, compute EWMA, detect violations, escalate.""" - from xdp_common import dump_rate_counters, block_ip, is_whitelisted - prev_counters = {} while not self._stop_event.is_set(): @@ -815,8 +845,6 @@ class DDoSDaemon: def _ai_thread(self): """Read traffic features, run AI inference or collect training data.""" - from xdp_common import read_percpu_features, dump_rate_counters, block_ip, is_whitelisted - prev_features = None self._last_retrain_time = self._get_model_mtime() self._last_log_cleanup = time.time() @@ -938,8 +966,6 @@ class DDoSDaemon: def _cleanup_thread(self): """Periodically clean up expired blocked IPs and stale violations.""" - from xdp_common import dump_blocked_ips, unblock_ip - while not self._stop_event.is_set(): try: with open('/proc/uptime') as f: @@ -1003,6 +1029,11 @@ class DDoSDaemon: t.join(timeout=5) self._remove_pid() + if hasattr(self, '_traffic_db') and self._traffic_db: + try: + self._traffic_db.close() + except Exception: + pass log.info("Daemon stopped")