# L'eseguibile si ottiene con pyinstaller --onefile --clean --noconsole client_UDP_crypto_protocollo_TLV_multicast_relay_simm_csharp.py # compatibile Windows 11 # compatibile Ubuntu 24.10 # compatibile python 3.12.7 import socket import struct import os import threading import time import queue import select import argparse # <<< AGGIUNTO from cryptography.hazmat.primitives import serialization from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import padding, rsa from cryptography.fernet import Fernet # --- Rimozione Costanti Globali per Parametri --- # MULTICAST_GROUP = '224.1.1.1' # Rimosso # MULTICAST_PORT = 5007 # Rimosso # TCP_CONTROL_PORT = 5009 # Rimosso # --- Configurazione non parametrizzabile (o lasciate come default modificabili da argparse) --- SERVER_UNICAST_IP = None # Lo impostiamo a None, se serve per workaround verrà passato come parametro? O lo omettiamo per ora? FRAGMENT_SIZE = 1024 MAX_PACKET_SIZE = 65507 KEEPALIVE_INTERVAL = 20 # --- Costanti TLV (UDP) --- # ... (invariate) ... TLV_FRAGMENT = 1 TLV_SERVER_PUBLIC_KEY_RESPONSE = 5 TLV_REQUEST_SERVER_PUBLIC_KEY = 7 TLV_DISCONNECT_REQUEST = 8 TLV_KEEPALIVE = 9 TLV_SENDER_ID = 10 TLV_NOTIFY_CONNECT = 11 TLV_NOTIFY_DISCONNECT = 12 TLV_ENCRYPTED_SYMMETRIC_KEY = 13 TLV_SYMMETRICALLY_ENCRYPTED_MESSAGE = 14 # --- Code e Evento --- output_queue = queue.Queue() command_queue = queue.Queue() shutdown_event = threading.Event() # --- Funzione Log --- def log_message(message): timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) try: output_queue.put(f"[{timestamp}] {message}") except: print(f"[Fallback Log Error] {timestamp} {message}") # --- Funzioni di utilità --- # ... (invariate, usano log_message) ... def create_tlv(type, value): type_bytes = struct.pack('!H', type) length_bytes = struct.pack('!H', len(value)) return type_bytes + length_bytes + value def parse_tlv(data): if len(data) < 4: raise ValueError("Dati TLV insufficienti.") type, length = struct.unpack('!HH', data[:4]) if len(data) < 4 + length: raise ValueError("Lunghezza TLV non corrisponde ai dati.") value = data[4:4 + length] remaining_data = data[4 + length:] return type, value, remaining_data def encrypt_data_asymmetric(data, public_key): return public_key.encrypt(data, padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None)) def encrypt_data_symmetric(data, fernet_key): f = Fernet(fernet_key) return f.encrypt(data) def decrypt_data_symmetric(token, fernet_key): f = Fernet(fernet_key) try: return f.decrypt(token) except: return None def fragment_data(data, fragment_size): return [data[i:i+fragment_size] for i in range(0, len(data), fragment_size)] # --- Logica Client UDP --- # Modificato per ricevere multicast_group e multicast_port come argomenti def initial_connect_and_key_exchange(sock, own_fernet_key, multicast_group, multicast_port): """ Richiede la chiave pubblica del server e invia la propria chiave simmetrica crittografata. """ # 1. Richiedi chiave pubblica del server tlv_request = create_tlv(TLV_REQUEST_SERVER_PUBLIC_KEY, b"") # Tipo 7 target_addr = (multicast_group, multicast_port) # << USA PARAMETRI log_message(f"Invio richiesta chiave pubblica a {target_addr}...") # LOG try: sock.sendto(tlv_request, target_addr) except Exception as e: log_message(f"Errore invio richiesta chiave: {e}") # LOG return None # Fallito invio sock.settimeout(10.0) server_public_key = None try: data, server_addr = sock.recvfrom(MAX_PACKET_SIZE) log_message(f"Risposta chiave pubblica ricevuta da {server_addr}") # LOG type, value, _ = parse_tlv(data) if type == TLV_SERVER_PUBLIC_KEY_RESPONSE: # Tipo 5 server_public_key = serialization.load_pem_public_key(value, backend=default_backend()) log_message("Chiave pubblica server ricevuta e caricata.") # LOG else: log_message(f"Errore: Ricevuto tipo TLV {type} invece della chiave pubblica ({TLV_SERVER_PUBLIC_KEY_RESPONSE}).") # LOG return None except socket.timeout: log_message("Errore: Timeout durante l'attesa della chiave pubblica del server.") # LOG return None except Exception as e: log_message(f"Errore ricezione/parsing chiave pubblica: {e}") # LOG return None finally: sock.settimeout(1.0) # 2. Crittografa e invia la chiave simmetrica del client if server_public_key: try: encrypted_fernet_key = encrypt_data_asymmetric(own_fernet_key, server_public_key) tlv_send_key = create_tlv(TLV_ENCRYPTED_SYMMETRIC_KEY, encrypted_fernet_key) # Tipo 13 log_message(f"Invio chiave simmetrica crittografata a {target_addr}...") # LOG sock.sendto(tlv_send_key, target_addr) # Invia a multicast log_message("Chiave simmetrica crittografata inviata al server.") # LOG return server_public_key except Exception as e: log_message(f"Errore durante l'invio della chiave simmetrica crittografata: {e}") # LOG return None else: return None # Modificato per ricevere multicast_group e multicast_port def send_encrypted_message(message, own_fernet_key, sock, multicast_group, multicast_port): """ Crittografa simmetricamente, wrappa (TLV 14), frammenta (TLV 1) e invia a MULTICAST.""" try: message_bytes = message.encode('utf-8') if isinstance(message, str) else message encrypted_payload = encrypt_data_symmetric(message_bytes, own_fernet_key) tlv_inner_message = create_tlv(TLV_SYMMETRICALLY_ENCRYPTED_MESSAGE, encrypted_payload) # Tipo 14 msg_id = os.urandom(4) target_addr = (multicast_group, multicast_port) # << USA PARAMETRI if len(tlv_inner_message) > FRAGMENT_SIZE: udp_fragments_payloads = fragment_data(tlv_inner_message, FRAGMENT_SIZE) num_fragments = len(udp_fragments_payloads) log_message(f"Invio messaggio '{message[:30]}...' frammentato in {num_fragments} parti a {target_addr}...") # LOG for seq_num, frag_payload in enumerate(udp_fragments_payloads): fragment_header = struct.pack('!I', int.from_bytes(msg_id, byteorder='big')) + \ struct.pack('!HH', seq_num, num_fragments) tlv_fragment_data = fragment_header + frag_payload tlv_outer_fragment = create_tlv(TLV_FRAGMENT, tlv_fragment_data) # Tipo 1 sock.sendto(tlv_outer_fragment, target_addr) time.sleep(0.005) else: log_message(f"Invio messaggio '{message[:30]}...' non frammentato a {target_addr}...") # LOG fragment_header = struct.pack('!I', int.from_bytes(msg_id, byteorder='big')) + \ struct.pack('!HH', 0, 1) tlv_fragment_data = fragment_header + tlv_inner_message tlv_outer_fragment = create_tlv(TLV_FRAGMENT, tlv_fragment_data) # Tipo 1 sock.sendto(tlv_outer_fragment, target_addr) except Exception as e: log_message(f"Errore durante l'invio del messaggio UDP: {e}") # LOG # receive_udp_message_thread non ha bisogno dei parametri, riceve e basta def receive_udp_message_thread(sock, own_fernet_key, stop_event): """ Thread per ricevere e processare messaggi UDP dal server. """ # ... (codice interno invariato, usa log_message) ... log_message("Thread ricezione UDP avviato.") # LOG while not stop_event.is_set(): try: readable, _, _ = select.select([sock], [], [], 1.0) if not readable: continue data, sender_server_addr = sock.recvfrom(MAX_PACKET_SIZE) offset = 0 sender_info_address = None while offset < len(data): try: type, value, _ = parse_tlv(data[offset:]) offset += 4 + len(value) # ... (Gestione TLV 10, 14, 11, 12 come prima, usando log_message) ... if type == TLV_SENDER_ID: try: sender_ip = value[:-2].decode('utf-8') sender_port = struct.unpack("!H", value[-2:])[0] sender_info_address = (sender_ip, sender_port) except: log_message("Errore parsing TLV SENDER_ID ricevuto via UDP.") sender_info_address = "Sconosciuto (errore parsing)" elif type == TLV_SYMMETRICALLY_ENCRYPTED_MESSAGE: decrypted_message = decrypt_data_symmetric(value, own_fernet_key) if decrypted_message: try: message_text = decrypted_message.decode('utf-8') sender_display = f"UDP Da {sender_info_address}:" if sender_info_address else "UDP Da Sconosciuto:" log_message(f"{sender_display} {message_text}") except UnicodeDecodeError: sender_display = f"UDP Dati binari da {sender_info_address}:" if sender_info_address else "UDP Dati binari da Sconosciuto:" log_message(f"{sender_display} <{len(decrypted_message)} bytes>") else: log_message("UDP Errore: Impossibile decrittare messaggio ricevuto.") sender_info_address = None elif type == TLV_NOTIFY_CONNECT: try: addr_ip = value[:-2].decode('utf-8') addr_port = struct.unpack("!H", value[-2:])[0] log_message(f"*** UDP Nuovo client connesso: {addr_ip}:{addr_port} ***") except: log_message("*** UDP Nuovo client connesso (errore parsing indirizzo) ***") elif type == TLV_NOTIFY_DISCONNECT: try: addr_ip = value[:-2].decode('utf-8') addr_port = struct.unpack("!H", value[-2:])[0] log_message(f"*** UDP Client disconnesso: {addr_ip}:{addr_port} ***") except: log_message("*** UDP Client disconnesso (errore parsing indirizzo) ***") else: log_message(f"WARN UDP: Ricevuto tipo TLV non gestito ({type}) dal server.") except (ValueError, struct.error) as e: log_message(f"Errore parsing TLV UDP ricevuto: {e}") break except socket.timeout: continue except OSError as e: if stop_event.is_set(): break log_message(f"Errore socket in ricezione UDP (OSError): {e}") time.sleep(1) except Exception as e: log_message(f"Errore generico in ricezione UDP: {e}") time.sleep(1) log_message("Thread ricezione UDP terminato.") # Modificato per ricevere multicast_group e multicast_port def send_keepalive(sock, multicast_group, multicast_port): """ Invia un messaggio di keep-alive (TLV 9) a MULTICAST. """ tlv_keepalive = create_tlv(TLV_KEEPALIVE, b"") target_addr = (multicast_group, multicast_port) # << USA PARAMETRI try: sock.sendto(tlv_keepalive, target_addr) except Exception as e: log_message(f"Errore invio keep-alive UDP a {target_addr}: {e}") # LOG # Modificato per ricevere multicast_group e multicast_port def keepalive_thread_function(sock, stop_event, multicast_group, multicast_port): """ Thread per inviare periodicamente keep-alive UDP. """ log_message("Thread keep-alive UDP avviato.") # LOG while not stop_event.is_set(): send_keepalive(sock, multicast_group, multicast_port) # << PASSA PARAMETRI stop_event.wait(KEEPALIVE_INTERVAL) log_message("Thread keep-alive UDP terminato.") # LOG # TCPControlServerThread non cambia internamente, ma il suo __init__ riceverà la porta TCP class TCPControlServerThread(threading.Thread): def __init__(self, tcp_control_port, output_q, command_q, shutdown_evt): # << RICEVE PORTA super().__init__() self.tcp_control_port = tcp_control_port # << MEMORIZZA PORTA self.output_queue = output_q self.command_queue = command_q self.main_shutdown_event = shutdown_evt self.running = True self.listen_socket = None # Verrà creato nel run() self.client_socket = None self.client_address = None def run(self): # Crea e binda il socket qui self.listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: self.listen_socket.bind(('0.0.0.0', self.tcp_control_port)) log_message(f"TCP Control Server (Client) binding su 0.0.0.0:{self.tcp_control_port}") except OSError as e: log_message(f"FATAL: Errore durante il bind del socket TCP Control: {e}") log_message(f"La porta TCP {self.tcp_control_port} potrebbe essere già in uso.") self.main_shutdown_event.set() # Segnala lo shutdown se il bind fallisce return # Termina il thread log_message(f"TCP Control server in ascolto su porta {self.tcp_control_port}") self.listen_socket.listen(1) while self.running: # ... (resto del codice run() invariato) ... readable, _, _ = select.select([self.listen_socket], [], [], 1.0) if self.listen_socket in readable: try: if self.client_socket: # Rifiuta nuova connessione se già attiva new_sock, new_addr = self.listen_socket.accept() log_message(f"TCP Rifiutata nuova connessione da {new_addr} (client già connesso)") try: new_sock.sendall(b"ERROR: Client already connected.\n") except: pass new_sock.close() continue self.client_socket, self.client_address = self.listen_socket.accept() self.client_socket.settimeout(0.5) log_message(f"TCP Control client connesso da {self.client_address}") self.handle_client() except OSError as e: if self.running: log_message(f"Errore TCP accept: {e}") time.sleep(0.5) except Exception as e: log_message(f"Errore imprevisto TCP accept: {e}") self.close_client_connection() if self.main_shutdown_event.is_set(): log_message("TCP Server: Rilevato evento di shutdown principale.") self.stop() log_message("TCP Control server thread terminato.") if self.listen_socket: self.listen_socket.close() # handle_client, close_client_connection, stop rimangono invariati internamente def handle_client(self): """ Gestisce la ricezione di comandi e l'invio di output per il client TCP. """ while self.running and self.client_socket: # 1. Invia output dalla coda try: while not self.output_queue.empty(): message = self.output_queue.get_nowait() try: self.client_socket.sendall((message + "\n").encode('utf-8')) except (socket.error, OSError) as send_err: log_message(f"Errore invio TCP a {self.client_address}: {send_err}. Disconnessione.") self.close_client_connection() return self.output_queue.task_done() except queue.Empty: pass except Exception as q_err: log_message(f"Errore imprevisto gestione coda output TCP: {q_err}") # 2. Ricevi comandi dal client TCP try: data = self.client_socket.recv(1024) if data: command = data.decode('utf-8').strip() if command.lower() == "exit": log_message("Ricevuto comando 'exit' via TCP. Avvio shutdown generale.") self.main_shutdown_event.set() self.stop() return else: self.command_queue.put(command) else: log_message(f"TCP Control client {self.client_address} disconnesso.") self.close_client_connection() return except socket.timeout: continue except (socket.error, OSError) as recv_err: log_message(f"Errore ricezione TCP da {self.client_address}: {recv_err}. Disconnessione.") self.close_client_connection() return except UnicodeDecodeError: log_message(f"Errore decodifica comando TCP da {self.client_address}.") try: self.client_socket.sendall(b"Errore: Inviare comandi testuali UTF-8.\n") except: self.close_client_connection(); return except Exception as e: log_message(f"Errore imprevisto gestione client TCP {self.client_address}: {e}") self.close_client_connection() return def close_client_connection(self): if self.client_socket: log_message(f"Chiusura connessione TCP con {self.client_address}") try: self.client_socket.shutdown(socket.SHUT_RDWR) except: pass try: self.client_socket.close() except: pass self.client_socket = None self.client_address = None def stop(self): log_message("Arresto TCP Control server thread...") self.running = False self.close_client_connection() try: with socket.create_connection(("127.0.0.1", self.tcp_control_port), timeout=0.1): pass except: pass if self.listen_socket: try: self.listen_socket.setblocking(False) self.listen_socket.close() log_message("TCP listening socket chiuso.") except Exception as e: log_message(f"Errore chiusura TCP listening socket: {e}") # --- Funzione Principale Client (Modificata per ricevere parametri) --- def run_client(mcast_group, mcast_port, tcp_port): # << RICEVE PARAMETRI global output_queue, command_queue, shutdown_event log_message(f"Client avviato con Multicast={mcast_group}:{mcast_port}, Controllo TCP su porta={tcp_port}") # --- Setup Socket UDP --- udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) ttl = struct.pack('b', 1) try: udp_sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) except OSError as e: log_message(f"WARN: Impossibile impostare IP_MULTICAST_TTL: {e}") # --- Genera chiave Fernet --- own_fernet_key = Fernet.generate_key() log_message("Chiave simmetrica Fernet generata.") # --- Handshake UDP --- # Passa i parametri alla funzione di handshake server_public_key = initial_connect_and_key_exchange(udp_sock, own_fernet_key, mcast_group, mcast_port) if server_public_key is None: log_message("Impossibile stabilire connessione sicura UDP con il server.") udp_sock.close() shutdown_event.set() # Assicura che anche il TCP non parta o si fermi return # --- Avvio Threads (Passando i parametri necessari) --- udp_receive_thread = threading.Thread(target=receive_udp_message_thread, args=(udp_sock, own_fernet_key, shutdown_event), daemon=True) # Passa mcast_group e mcast_port al thread keepalive udp_keepalive_thread = threading.Thread(target=keepalive_thread_function, args=(udp_sock, shutdown_event, mcast_group, mcast_port), daemon=True) # Passa la porta tcp_port al costruttore del thread TCP tcp_control_thread = TCPControlServerThread(tcp_port, output_queue, command_queue, shutdown_event) udp_receive_thread.start() udp_keepalive_thread.start() tcp_control_thread.start() log_message("Connesso via UDP! Server TCP di controllo attivo. In attesa di connessione client TCP...") # --- Loop Principale di Elaborazione Comandi --- try: while not shutdown_event.is_set(): try: command = command_queue.get(timeout=0.5) log_message(f"Comando ricevuto dalla coda: '{command[:50]}...'") # Passa i parametri a send_encrypted_message send_encrypted_message(command, own_fernet_key, udp_sock, mcast_group, mcast_port) command_queue.task_done() except queue.Empty: if not udp_receive_thread.is_alive() or not udp_keepalive_thread.is_alive() or not tcp_control_thread.is_alive(): log_message("WARN: Uno dei thread principali è terminato inaspettatamente. Avvio shutdown.") shutdown_event.set() break continue except Exception as loop_err: log_message(f"Errore nel loop principale comandi: {loop_err}") time.sleep(1) except KeyboardInterrupt: log_message("Chiusura richiesta (Ctrl+C)...") shutdown_event.set() finally: log_message("Avvio procedura di shutdown client...") # 1. Invia messaggio DISCONNECT via UDP (best effort) if udp_sock and udp_sock.fileno() != -1: log_message("Invio messaggio di disconnessione UDP...") try: # Passa i parametri anche qui send_encrypted_message("DISCONNECT", own_fernet_key, udp_sock, mcast_group, mcast_port) time.sleep(0.5) except Exception as e: log_message(f"Errore invio messaggio di disconnessione UDP: {e}") # 2. Segnala e attendi terminazione thread log_message("Arresto thread...") if not shutdown_event.is_set(): shutdown_event.set() # Assicurati sia settato if tcp_control_thread.is_alive(): tcp_control_thread.stop() tcp_control_thread.join(timeout=3.0) if tcp_control_thread.is_alive(): log_message("WARN: Thread TCP Control non terminato.") if udp_keepalive_thread.is_alive(): udp_keepalive_thread.join(timeout=1.0) if udp_keepalive_thread.is_alive(): log_message("WARN: Thread Keepalive UDP non terminato.") if udp_receive_thread.is_alive(): udp_receive_thread.join(timeout=1.0) if udp_receive_thread.is_alive(): log_message("WARN: Thread Ricezione UDP non terminato.") # 3. Chiudi socket UDP if udp_sock and udp_sock.fileno() != -1: try: udp_sock.close() log_message("Socket UDP client chiuso.") except Exception as e: log_message(f"Errore chiusura socket UDP: {e}") log_message("Client terminato.") # --- Blocco Main con Argparse --- if __name__ == "__main__": parser = argparse.ArgumentParser(description="Client UDP/TCP Relay") # Modifica descrizione parser.add_argument( '--multicast-group', type=str, default="224.1.1.1", help="Indirizzo multicast del server UDP (default: 224.1.1.1)" # Modifica help ) parser.add_argument( '--multicast-port', type=int, default=5007, help="Porta multicast del server UDP (default: 5007)" # Modifica help ) parser.add_argument( '--tcp-control-port', type=int, default=5009, # Cambia il default se preferisci (era 5008 nel tuo esempio, ma 5009 è nel codice originale) help="Porta locale su cui questo client ascolterà per le connessioni TCP di controllo (default: 5009)" # Modifica help ) # Potresti aggiungere un argomento per SERVER_UNICAST_IP se necessario per workaround # parser.add_argument('--server-ip', type=str, default=None, help="IP unicast del server UDP (opzionale, per workaround)") args = parser.parse_args() # Esegui la funzione principale passando i valori letti da argparse run_client(args.multicast_group, args.multicast_port, args.tcp_control_port)