Feature: Rate-limiting par hôte cible
Contexte
AttackSim possède sandbox/budget.py qui implémente un RequestBudget global uniquement : rate_limit, max_requests, wall_budget sont partagés entre tous les modules et tous les hôtes. Sur un --cidr 10.0.0.0/24 (256 hôtes), le budget global (50 req/s par défaut) est réparti entre tous les modules concurrents sur tous les hôtes → un seul host populaire (ex. 10.0.0.5 avec 20 ports ouverts + HTTP) peut consommer tout le budget pendant que les 255 autres sont à peine touchés. Les outils pro (nmap --rate, nuclei -rl, ffuf -rate) ont un rate par hôte.
Description
Étendre RequestBudget pour supporter un rate-limiting par hôte en plus du global, avec un plafond global.
Spécifications fonctionnelles
Comportement attendu
- Le budget global (ex. 200 req/s) reste le plafond supérieur.
- Chaque hôte reçoit un sous-budget (ex. 50 req/s) qui ne peut pas être dépassé.
- Si
rate_limit_per_host n'est pas défini, fallback au comportement actuel (global uniquement).
- Les modules qui accèdent à plusieurs hôtes (ex.
host_sweep, cloud_pivot) utilisent le budget de l'hôte qu'ils contactent.
Interface CLI
--rate-limit 200 # global (existant)
--rate-limit-per-host 50 # nouveau, défaut: = rate_limit (rétro-compatible)
Output
Aucun changement dans le format de rapport. Le budget est un mécanisme interne. En mode verbose (-v), afficher :
[*] budget: global=200/s per_host=50/s | host 10.0.0.5: 48/s (96%)
Spécifications techniques
Fichier à modifier
sandbox/budget.py
Changements
class RequestBudget:
def __init__(self, rate_limit=None, max_requests=None, wall_budget=None,
rate_limit_per_host=None):
self._global_lock = threading.Lock()
self._global_window = collections.deque()
self._global_rate = rate_limit or GLOBAL_MAX_REQUESTS
self._global_max = max_requests or GLOBAL_MAX_REQUESTS
self._wall = wall_budget or GLOBAL_WALL_BUDGET
self._start = time.monotonic()
self._per_host = rate_limit_per_host or self._global_rate
self._host_windows: dict[str, collections.deque] = {}
self._host_locks: dict[str, threading.Lock] = {}
def acquire(self, host: str = None, n: int = 1, timeout: float = 30.0) -> bool:
"""Acquire n request slots. If host is given, also enforce per-host rate."""
# 1. Check wall budget
if time.monotonic() - self._start > self._wall:
return False
# 2. Check global max
with self._global_lock:
if len(self._global_window) >= self._global_max:
return False
# 3. Acquire global slot (existing logic)
if not self._acquire_window(self._global_window, self._global_lock,
self._global_rate, n, timeout):
return False
# 4. Acquire per-host slot (new)
if host and self._per_host != self._global_rate:
hlock = self._host_locks.setdefault(host, threading.Lock())
hwin = self._host_windows.setdefault(host, collections.deque())
if not self._acquire_window(hwin, hlock, self._per_host, n, timeout):
# rollback global slot
with self._global_lock:
for _ in range(n):
if self._global_window: self._global_window.popleft()
return False
return True
def _acquire_window(self, window, lock, rate, n, timeout):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
with lock:
now = time.monotonic()
while window and window[0] < now - 1.0:
window.popleft()
if len(window) + n <= rate:
for _ in range(n):
window.append(now)
return True
time.sleep(0.05)
return False
Changements dans main.py
_configure_target : passer rate_limit_per_host=args.rate_limit_per_host au RequestBudget.
_fanout_hostport : chaque sub-target hérite du budget parent (partagé).
- Les modules qui appellent
target.budget.acquire() doivent passer host=target.host :
target.budget.acquire(host=target.host)
Changements dans les modules
Mettre à jour les appels budget.acquire() dans les modules qui scanment :
exploits/port_probe.py
exploits/http_probe.py
exploits/host_sweep.py
exploits/web_spider.py
exploits/content_discovery.py
exploits/fuzz_probe.py
exploits/default_creds.py
Configuration (config.py)
GLOBAL_RATE_LIMIT_PER_HOST = _env("GLOBAL_RATE_LIMIT_PER_HOST", 50)
Tests
tests/test_budget_per_host.py (nouveau) :
RequestBudget(rate_limit=100, rate_limit_per_host=10) — 10 hôtes en parallèle, chaque host peut faire 10/s, global plafond 100/s
- Host A fait 10 req → 11e bloquée jusqu'à la fenêtre
- Host B fait 10 req en même temps → OK (séparé)
- 11 hôtes à 10/s → le 11e bloqué par le global
rate_limit_per_host=None → comportement actuel (rétro-compatible)
- Vérifier thread-safety avec ThreadPoolExecutor
tests/test_budget.py (existant) — tous les tests doivent encore passer.
Backward compatibility
--rate-limit-per-host par défaut = --rate-limit (comportement actuel si non spécifié)
RequestBudget() sans argument → rate_limit_per_host=None → pas de per-host
Acceptance criteria
Références
- nmap
--rate / --max-rate
- nuclei
-rl / -rate-limit
- ffuf
-rate
Feature: Rate-limiting par hôte cible
Contexte
AttackSim possède
sandbox/budget.pyqui implémente unRequestBudgetglobal uniquement :rate_limit,max_requests,wall_budgetsont partagés entre tous les modules et tous les hôtes. Sur un--cidr 10.0.0.0/24(256 hôtes), le budget global (50 req/s par défaut) est réparti entre tous les modules concurrents sur tous les hôtes → un seul host populaire (ex. 10.0.0.5 avec 20 ports ouverts + HTTP) peut consommer tout le budget pendant que les 255 autres sont à peine touchés. Les outils pro (nmap--rate, nuclei-rl, ffuf-rate) ont un rate par hôte.Description
Étendre
RequestBudgetpour supporter un rate-limiting par hôte en plus du global, avec un plafond global.Spécifications fonctionnelles
Comportement attendu
rate_limit_per_hostn'est pas défini, fallback au comportement actuel (global uniquement).host_sweep,cloud_pivot) utilisent le budget de l'hôte qu'ils contactent.Interface CLI
Output
Aucun changement dans le format de rapport. Le budget est un mécanisme interne. En mode verbose (
-v), afficher :Spécifications techniques
Fichier à modifier
sandbox/budget.pyChangements
Changements dans
main.py_configure_target: passerrate_limit_per_host=args.rate_limit_per_hostauRequestBudget._fanout_hostport: chaque sub-target hérite du budget parent (partagé).target.budget.acquire()doivent passerhost=target.host:Changements dans les modules
Mettre à jour les appels
budget.acquire()dans les modules qui scanment :exploits/port_probe.pyexploits/http_probe.pyexploits/host_sweep.pyexploits/web_spider.pyexploits/content_discovery.pyexploits/fuzz_probe.pyexploits/default_creds.pyConfiguration (
config.py)Tests
tests/test_budget_per_host.py(nouveau) :RequestBudget(rate_limit=100, rate_limit_per_host=10)— 10 hôtes en parallèle, chaque host peut faire 10/s, global plafond 100/srate_limit_per_host=None→ comportement actuel (rétro-compatible)tests/test_budget.py(existant) — tous les tests doivent encore passer.Backward compatibility
--rate-limit-per-hostpar défaut =--rate-limit(comportement actuel si non spécifié)RequestBudget()sans argument →rate_limit_per_host=None→ pas de per-hostAcceptance criteria
RequestBudget(rate_limit_per_host=N)limite chaque hôte à N req/srate_limit_per_host=None→ comportement inchangé (rétro-compatible)host=target.hostàacquire()--rate-limit-per-hostdisponible en CLIdocs/budget-and-rate-limiting.mdRéférences
--rate/--max-rate-rl/-rate-limit-rate