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

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

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

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

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

View File

@@ -11,6 +11,7 @@ time-profile switching, and automatic escalation.
- Cleanup Thread: removes expired entries from blocked_ips maps
"""
import copy
import os
import sys
import time
@@ -57,12 +58,10 @@ DEFAULT_CONFIG = {
'profiles': {},
},
'escalation': {
'rate_limit_after': 1,
'temp_block_after': 5,
'perm_block_after': 20,
'temp_block_duration': 300,
'violation_window': 600,
'cooldown_multiplier': 0.5,
},
'ewma': {
'alpha': 0.3,
@@ -78,7 +77,6 @@ DEFAULT_CONFIG = {
'min_samples': 1000,
'poll_interval': 5,
'anomaly_threshold': -0.3,
'retrain_interval': 604800,
'min_packets_for_sample': 20,
'model_file': '/var/lib/xdp-defense/ai_model.pkl',
'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):
"""Load config with defaults."""
cfg = DEFAULT_CONFIG.copy()
cfg = copy.deepcopy(DEFAULT_CONFIG)
try:
with open(path) as f:
user = yaml.safe_load(f) or {}
@@ -406,8 +404,9 @@ class DDoSDaemon:
def __init__(self, config_path=CONFIG_PATH):
self.config_path = config_path
self.cfg = load_config(config_path)
self.running = False
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()
def _setup_components(self):
@@ -465,12 +464,21 @@ class DDoSDaemon:
def _handle_sighup(self, signum, frame):
log.info("SIGHUP received, reloading config...")
self.cfg = load_config(self.config_path)
self._setup_components()
log.info("Config reloaded")
# 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()
log.setLevel(getattr(logging, level, logging.INFO))
log.info("Config reloaded (state preserved)")
def _handle_sigterm(self, signum, frame):
log.info("SIGTERM received, shutting down...")
self.running = False
self._stop_event.set()
def _handle_sigusr1(self, signum, frame):
@@ -481,12 +489,12 @@ class DDoSDaemon:
def _ewma_thread(self):
"""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 = {}
while self.running:
while not self._stop_event.is_set():
interval = self._ewma_interval
try:
entries = dump_rate_counters('rate_counter_v4', top_n=1000)
active_ips = []
@@ -505,6 +513,11 @@ class DDoSDaemon:
is_anomalous = self.ewma_analyzer.update(ip_str, pps)
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)
ew = self.ewma_analyzer.get_stats(ip_str)
log.warning(
@@ -536,12 +549,12 @@ 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
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
while self.running:
while not self._stop_event.is_set():
interval = self._ai_interval
try:
if not self.ai_detector.enabled:
self._stop_event.wait(interval)
@@ -593,6 +606,11 @@ class DDoSDaemon:
)
top_ips = dump_rate_counters('rate_counter_v4', top_n=5)
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)
log.warning("AI escalation: %s -> %s", ip_str, level)
@@ -620,7 +638,7 @@ class DDoSDaemon:
def _profile_thread(self):
"""Check time-of-day and switch rate profiles."""
while self.running:
while not self._stop_event.is_set():
try:
self.profile_manager.check_and_apply()
except Exception as e:
@@ -631,7 +649,7 @@ class DDoSDaemon:
"""Periodically clean up expired blocked IPs and stale violations."""
from xdp_common import dump_blocked_ips, unblock_ip
while self.running:
while not self._stop_event.is_set():
try:
with open('/proc/uptime') as f:
now_ns = int(float(f.read().split()[0]) * 1_000_000_000)
@@ -667,7 +685,6 @@ class DDoSDaemon:
self._ensure_single_instance()
self._write_pid()
self.running = True
threads = [
threading.Thread(target=self._ewma_thread, name='ewma', daemon=True),
@@ -683,13 +700,12 @@ class DDoSDaemon:
log.info("Daemon running (PID %d)", os.getpid())
try:
while self.running:
while not self._stop_event.is_set():
self._stop_event.wait(1)
except KeyboardInterrupt:
pass
log.info("Shutting down...")
self.running = False
self._stop_event.set()
for t in threads: