#!/usr/bin/env python3 """ TechIRCd Stress Testing Tool Advanced IRC server stress testing with configurable scenarios """ import asyncio import json import random import string import time import logging import argparse from typing import List, Dict, Any import socket import ssl class IRCClient: def __init__(self, config: Dict[str, Any], client_id: int): self.config = config self.client_id = client_id self.nick = f"{config['nick_prefix']}{client_id:04d}" self.user = f"user{client_id}" self.realname = f"Stress Test Client {client_id}" self.reader = None self.writer = None self.connected = False self.registered = False self.channels = [] self.message_count = 0 self.start_time = None async def connect(self): """Connect to IRC server""" try: if self.config.get('ssl', False): context = ssl.create_default_context() self.reader, self.writer = await asyncio.open_connection( self.config['host'], self.config.get('ssl_port', 6697), ssl=context ) else: self.reader, self.writer = await asyncio.open_connection( self.config['host'], self.config['port'] ) self.connected = True self.start_time = time.time() # Start registration await self.send(f"NICK {self.nick}") await self.send(f"USER {self.user} 0 * :{self.realname}") # Start message handler asyncio.create_task(self.message_handler()) return True except Exception as e: logging.error(f"Client {self.client_id} connection failed: {e}") return False async def send(self, message: str): """Send message to server""" if self.writer and not self.writer.is_closing(): try: self.writer.write(f"{message}\r\n".encode()) await self.writer.drain() logging.debug(f"Client {self.client_id} sent: {message}") except Exception as e: logging.error(f"Client {self.client_id} send error: {e}") async def message_handler(self): """Handle incoming messages""" try: while self.connected and self.reader: line = await self.reader.readline() if not line: break message = line.decode().strip() if not message: continue logging.debug(f"Client {self.client_id} received: {message}") await self.handle_message(message) except Exception as e: logging.error(f"Client {self.client_id} message handler error: {e}") finally: self.connected = False async def handle_message(self, message: str): """Process incoming IRC messages""" parts = message.split() if len(parts) < 2: return # Handle PING if parts[0] == "PING": await self.send(f"PONG {parts[1]}") return # Handle numeric responses if len(parts) >= 2 and parts[1].isdigit(): numeric = int(parts[1]) # Welcome message - registration complete if numeric == 1: # RPL_WELCOME self.registered = True logging.info(f"Client {self.client_id} ({self.nick}) registered successfully") # Auto-join channels if configured for channel in self.config.get('auto_join_channels', []): await self.join_channel(channel) async def join_channel(self, channel: str): """Join a channel""" await self.send(f"JOIN {channel}") if channel not in self.channels: self.channels.append(channel) async def send_privmsg(self, target: str, message: str): """Send private message or channel message""" await self.send(f"PRIVMSG {target} :{message}") self.message_count += 1 async def disconnect(self): """Disconnect from server""" self.connected = False if self.writer and not self.writer.is_closing(): await self.send("QUIT :Stress test complete") self.writer.close() await self.writer.wait_closed() class StressTest: def __init__(self, config_file: str): with open(config_file, 'r') as f: self.config = json.load(f) self.clients: List[IRCClient] = [] self.start_time = None self.stats = { 'connected': 0, 'registered': 0, 'messages_sent': 0, 'errors': 0 } # Setup logging log_level = getattr(logging, self.config.get('log_level', 'INFO').upper()) logging.basicConfig( level=log_level, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('stress_test.log'), logging.StreamHandler() ] ) async def run_scenario(self, scenario: Dict[str, Any]): """Run a specific test scenario""" scenario_name = scenario['name'] client_count = scenario['client_count'] duration = scenario.get('duration', 60) logging.info(f"Starting scenario: {scenario_name}") logging.info(f"Clients: {client_count}, Duration: {duration}s") # Create and connect clients if scenario.get('connect_gradually', False): await self.gradual_connect(client_count, scenario.get('connect_delay', 0.1)) else: await self.mass_connect(client_count) # Run scenario activities await self.run_activities(scenario, duration) # Collect stats await self.collect_stats() # Disconnect clients if scenario.get('disconnect_gradually', False): await self.gradual_disconnect(scenario.get('disconnect_delay', 0.1)) else: await self.mass_disconnect() logging.info(f"Scenario {scenario_name} completed") async def mass_connect(self, count: int): """Connect many clients simultaneously""" logging.info(f"Mass connecting {count} clients...") tasks = [] for i in range(count): client = IRCClient(self.config['server'], i + 1) self.clients.append(client) tasks.append(client.connect()) results = await asyncio.gather(*tasks, return_exceptions=True) connected = sum(1 for r in results if r is True) self.stats['connected'] = connected logging.info(f"Connected {connected}/{count} clients") # Wait for registration await asyncio.sleep(2) registered = sum(1 for c in self.clients if c.registered) self.stats['registered'] = registered logging.info(f"Registered {registered}/{connected} clients") async def gradual_connect(self, count: int, delay: float): """Connect clients gradually with delay""" logging.info(f"Gradually connecting {count} clients with {delay}s delay...") for i in range(count): client = IRCClient(self.config['server'], i + 1) self.clients.append(client) success = await client.connect() if success: self.stats['connected'] += 1 if delay > 0: await asyncio.sleep(delay) # Wait for registration await asyncio.sleep(2) registered = sum(1 for c in self.clients if c.registered) self.stats['registered'] = registered logging.info(f"Registered {registered}/{self.stats['connected']} clients") async def run_activities(self, scenario: Dict[str, Any], duration: int): """Run scenario activities for specified duration""" activities = scenario.get('activities', []) if not activities: logging.info(f"No activities specified, waiting {duration} seconds...") await asyncio.sleep(duration) return end_time = time.time() + duration while time.time() < end_time: for activity in activities: if time.time() >= end_time: break await self.run_activity(activity) # Delay between activities delay = activity.get('delay', 1.0) await asyncio.sleep(delay) async def run_activity(self, activity: Dict[str, Any]): """Run a single activity""" activity_type = activity['type'] if activity_type == 'channel_flood': await self.channel_flood_activity(activity) elif activity_type == 'private_flood': await self.private_flood_activity(activity) elif activity_type == 'join_part_spam': await self.join_part_spam_activity(activity) elif activity_type == 'nick_change_spam': await self.nick_change_spam_activity(activity) elif activity_type == 'random_commands': await self.random_commands_activity(activity) else: logging.warning(f"Unknown activity type: {activity_type}") async def channel_flood_activity(self, activity: Dict[str, Any]): """Flood channels with messages""" message_count = activity.get('message_count', 10) channel = activity.get('channel', '#test') registered_clients = [c for c in self.clients if c.registered] if not registered_clients: return tasks = [] for _ in range(message_count): client = random.choice(registered_clients) message = self.generate_random_message() tasks.append(client.send_privmsg(channel, message)) await asyncio.gather(*tasks, return_exceptions=True) self.stats['messages_sent'] += len(tasks) async def private_flood_activity(self, activity: Dict[str, Any]): """Flood with private messages""" message_count = activity.get('message_count', 10) registered_clients = [c for c in self.clients if c.registered] if len(registered_clients) < 2: return tasks = [] for _ in range(message_count): sender = random.choice(registered_clients) target = random.choice(registered_clients) if sender != target: message = self.generate_random_message() tasks.append(sender.send_privmsg(target.nick, message)) await asyncio.gather(*tasks, return_exceptions=True) self.stats['messages_sent'] += len(tasks) async def join_part_spam_activity(self, activity: Dict[str, Any]): """Spam JOIN/PART commands""" iterations = activity.get('iterations', 5) channels = activity.get('channels', ['#spam1', '#spam2', '#spam3']) registered_clients = [c for c in self.clients if c.registered] if not registered_clients: return for _ in range(iterations): client = random.choice(registered_clients) channel = random.choice(channels) await client.join_channel(channel) await asyncio.sleep(0.1) await client.send(f"PART {channel} :Spam test") async def nick_change_spam_activity(self, activity: Dict[str, Any]): """Spam NICK changes""" iterations = activity.get('iterations', 5) registered_clients = [c for c in self.clients if c.registered] if not registered_clients: return for _ in range(iterations): client = random.choice(registered_clients) new_nick = f"{client.nick}_{''.join(random.choices(string.ascii_lowercase, k=3))}" await client.send(f"NICK {new_nick}") await asyncio.sleep(0.1) async def random_commands_activity(self, activity: Dict[str, Any]): """Send random IRC commands""" command_count = activity.get('command_count', 10) commands = activity.get('commands', ['WHO #test', 'WHOIS randomnick', 'LIST', 'VERSION']) registered_clients = [c for c in self.clients if c.registered] if not registered_clients: return for _ in range(command_count): client = random.choice(registered_clients) command = random.choice(commands) await client.send(command) await asyncio.sleep(0.1) def generate_random_message(self) -> str: """Generate random message content""" words = ['hello', 'world', 'test', 'stress', 'irc', 'server', 'message', 'random', 'data'] return ' '.join(random.choices(words, k=random.randint(1, 8))) async def collect_stats(self): """Collect and display statistics""" connected = sum(1 for c in self.clients if c.connected) registered = sum(1 for c in self.clients if c.registered) total_messages = sum(c.message_count for c in self.clients) self.stats.update({ 'connected': connected, 'registered': registered, 'messages_sent': total_messages }) logging.info(f"Stats: {connected} connected, {registered} registered, {total_messages} messages sent") async def mass_disconnect(self): """Disconnect all clients simultaneously""" logging.info("Disconnecting all clients...") tasks = [client.disconnect() for client in self.clients] await asyncio.gather(*tasks, return_exceptions=True) self.clients.clear() async def gradual_disconnect(self, delay: float): """Disconnect clients gradually""" logging.info(f"Gradually disconnecting clients with {delay}s delay...") for client in self.clients: await client.disconnect() if delay > 0: await asyncio.sleep(delay) self.clients.clear() async def run_all_scenarios(self): """Run all configured scenarios""" self.start_time = time.time() for scenario in self.config['scenarios']: try: await self.run_scenario(scenario) # Delay between scenarios scenario_delay = scenario.get('delay_after', 2) if scenario_delay > 0: logging.info(f"Waiting {scenario_delay}s before next scenario...") await asyncio.sleep(scenario_delay) except Exception as e: logging.error(f"Scenario {scenario['name']} failed: {e}") self.stats['errors'] += 1 total_time = time.time() - self.start_time logging.info(f"All scenarios completed in {total_time:.2f} seconds") logging.info(f"Final stats: {self.stats}") def main(): parser = argparse.ArgumentParser(description='TechIRCd Stress Testing Tool') parser.add_argument('--config', '-c', default='stress_config.json', help='Configuration file (default: stress_config.json)') parser.add_argument('--scenario', '-s', help='Run specific scenario only') args = parser.parse_args() try: stress_test = StressTest(args.config) if args.scenario: # Run specific scenario scenario = next((s for s in stress_test.config['scenarios'] if s['name'] == args.scenario), None) if scenario: asyncio.run(stress_test.run_scenario(scenario)) else: print(f"Scenario '{args.scenario}' not found") return 1 else: # Run all scenarios asyncio.run(stress_test.run_all_scenarios()) return 0 except FileNotFoundError: print(f"Configuration file '{args.config}' not found") return 1 except Exception as e: print(f"Error: {e}") return 1 if __name__ == "__main__": exit(main())