commit 5d0af413ef08c7a25468ec68b0a16446526fc065 Author: Anders Knutsen Date: Wed Jul 23 10:07:41 2025 +0200 Oppdatert dokumentasjon for bruk diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..db8786c --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..0b1f6a4 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/tcp_splitter.iml b/.idea/tcp_splitter.iml new file mode 100644 index 0000000..f571432 --- /dev/null +++ b/.idea/tcp_splitter.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..3fc1172 --- /dev/null +++ b/config.json @@ -0,0 +1,16 @@ +{ + "listen_host": "0.0.0.0", + "listen_port": 10001, + "target_hosts": [ + { + "host": "0.0.0.0", + "port": 12001 + }, + { + "host": "0.0.0.0", + "port": 12002 + } + ], + "run_duration": 600, + "log_level": "INFO" +} diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..7a876ee --- /dev/null +++ b/readme.txt @@ -0,0 +1,39 @@ +# TCP Splitter +Et Python-basert verktøy som splitter innkommende TCP-trafikk til flere mål samtidig. + +## Start + +Kjør `tcp_splitter.py` for å starte programmet. +Scriptet `worker.py` blir startet automatisk av hovedscriptet, og restartes med jevne mellomrom basert på verdien satt i `config.json`. + +--- + +## Konfigurasjon (`config.json`) +listen_host / listen_port: Hvor splitteren lytter etter innkommende forbindelser. +target_hosts: Liste over adresser og porter som mottar kopier av trafikken. +run_duration: Hvor lenge (sekunder) splitteren skal kjøre før alle forbindelser restartes. +log_level: Velg mellom INFO, DEBUG eller ERROR + +### Eksempel: + { + "listen_host": "0.0.0.0", + "listen_port": 10001, + "target_hosts": [ + { + "host": "0.0.0.0", + "port": 12001 + }, + { + "host": "0.0.0.0", + "port": 12002 + } + ], + "run_duration": 600, + "log_level": "INFO" + } + +project-root/ +├── tcp_splitter.py +├── worker.py +├── config.json +└── README.md \ No newline at end of file diff --git a/tcp_splitter.py b/tcp_splitter.py new file mode 100644 index 0000000..91d5f2a --- /dev/null +++ b/tcp_splitter.py @@ -0,0 +1,26 @@ +import subprocess +import time + +while True: + # Get the current time and print it when starting the process + restart_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + print(f"Starting worker script at {restart_time}") + + # Start the worker script in a new Command Prompt window with a custom title + process = subprocess.Popen(['start', 'cmd', '/c', 'title TCP_SPLITTER && worker.exe'], shell=True) + + # Wait a moment to allow the process to start + time.sleep(2) + + while True: + # Check if the specific titled process is still running + result = subprocess.run('tasklist /v | findstr "TCP_SPLITTER"', shell=True, stdout=subprocess.PIPE, text=True) + + # If "TCP_SPLITTER" title is not found, the worker script has stopped + if "TCP_SPLITTER" not in result.stdout: + exit_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + print(f"Worker script exited at {exit_time}. Restarting...") + break # Exit the inner loop to restart the outer loop and launch the script again + + # Check every second if the script is still running + time.sleep(1) diff --git a/worker.py b/worker.py new file mode 100644 index 0000000..37e69a9 --- /dev/null +++ b/worker.py @@ -0,0 +1,162 @@ +import socket +import threading +import json +import time +import logging +from datetime import datetime +from crccheck.crc import CrcArc + +# Load configuration from config.json +with open('config.json', 'r') as config_file: + config = json.load(config_file) + +# Configuration from config.json +LISTEN_HOST = config['listen_host'] +LISTEN_PORT = config['listen_port'] +TARGET_HOSTS = config['target_hosts'] +RUN_DURATION = config['run_duration'] +LOG_LEVEL = config['log_level'] + +# Map log level strings to logging module constants +log_level_mapping = { + "DEBUG": logging.DEBUG, + "INFO": logging.INFO, + "WARNING": logging.WARNING, + "ERROR": logging.ERROR, + "CRITICAL": logging.CRITICAL +} + +# Validate and set the log level +if LOG_LEVEL not in log_level_mapping: + raise ValueError(f"Invalid log level: {LOG_LEVEL}. Valid options are: {list(log_level_mapping.keys())}") + +# Logging configuration +logging.basicConfig( + level=log_level_mapping[LOG_LEVEL], + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('relay_server.log', encoding='utf-8'), # Log to a file + logging.StreamHandler() # Log to the console + ] +) +logger = logging.getLogger(__name__) + +def relay_to_server(server_host, server_port, data): + """Relay data to a server and return the response.""" + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket: + server_socket.connect((server_host, server_port)) + server_socket.sendall(data) + response = server_socket.recv(4096) + return response + except Exception as e: + logger.error(f"Failed to relay data to {server_host}:{server_port}: {e}") + return None + +def calculate_crc(data): + """Calculate CRC using CrcArc.""" + crc = CrcArc.calc(data.encode()) # Encode data to bytes + crc_hex = hex(crc).split('x')[1].upper().zfill(4) # Format as uppercase hex (4 chars) + return crc_hex + + +def calculate_length(data): + """Calculate the length of the data in hexadecimal.""" + length = len(data.encode()) # Length in bytes + length_hex = hex(length)[2:].upper().zfill(4) # Convert to hex, ensure 4 digits + return length_hex + +def handle_client(client_socket, client_address): + """Handle a client connection.""" + try: + # Receive data from the client + data = client_socket.recv(4096) + if not data: + logger.warning(f"No data received from {client_address}") + return + + logger.debug(f"Received data from {client_address}: {data}") + + # Decode the data and check for NULL message + plain_data = data.decode("utf-8", errors='ignore') + # Validate message integrity + if not "SIA-DCS" in plain_data or not "ADM-CID" in plain_data: + return + if '"NULL"' in plain_data: + logger.debug("NULL Message detected, sending ACK without relaying signal!") + orig_text = plain_data[15:].strip() + ack_text = '"ACK"' + orig_text + ack_text_bytes = ack_text.encode() + logger.debug(f"ACK Message: {ack_text}") + + # Calculate CRC and format it + crc = CrcArc.calc(ack_text_bytes) + crcstr = str(hex(crc)).split('x') + crcstr = str(crcstr[1].upper()) + if len(crcstr) == 2: + crcstr = '00' + crcstr + if len(crcstr) == 3: + crcstr = '0' + crcstr + + # Calculate length and format it + length = str(hex(len(ack_text_bytes))).split('x') + logger.debug(f"CRC & Length: {crcstr}, 00{length[1]}") + # Construct the ACK message + ack_msg = '\n' + crcstr + '00' + length[1].upper() + ack_text + '\r' + + # Send the ACK response + ack_bytes = bytes(ack_msg, 'ASCII') + client_socket.sendall(ack_bytes) + logger.debug(f"Response sent to client: {ack_msg.strip()}") + return + + # Relay the data to all target hosts + responses = [] + for target in TARGET_HOSTS: + logger.info(f"Relaying data to {target['host']}:{target['port']}") + response = relay_to_server(target['host'], target['port'], data) + if response: + responses.append(response) + logger.debug(f"Received response from {target['host']}:{target['port']}: {response}") + + # Send only the first server's response back to the client + if responses: + client_socket.sendall(responses[0]) + logger.info(f"Sent response to {client_address}: {responses[0]}") + else: + logger.warning(f"No responses received from target hosts for {client_address}") + + except Exception as e: + logger.error(f"Error handling client {client_address}: {e}") + finally: + client_socket.close() + logger.debug(f"Closed connection with {client_address}") + +def start_relay(): + """Start the TCP relay server.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket: + server_socket.bind((LISTEN_HOST, LISTEN_PORT)) + server_socket.listen(5) + logger.info(f"Relay server started listening on {LISTEN_HOST}:{LISTEN_PORT}") + + # Set a timer to stop the server after RUN_DURATION seconds + stop_time = time.time() + RUN_DURATION + logger.debug(f"Server will run for {RUN_DURATION} seconds.") + + while time.time() < stop_time: + try: + # Set a timeout to periodically check if the run duration has elapsed + server_socket.settimeout(1) + client_socket, client_address = server_socket.accept() + logger.debug(f"Accepted connection from {client_address}") + client_thread = threading.Thread(target=handle_client, args=(client_socket, client_address)) + client_thread.start() + except socket.timeout: + continue + except Exception as e: + logger.error(f"Error accepting connection: {e}") + + logger.info("Server run duration elapsed. Shutting down.") + +if __name__ == "__main__": + start_relay() \ No newline at end of file