From 8fed931682ff8a54f035f6efc21267fb55798643 Mon Sep 17 00:00:00 2001 From: wkukielk Date: Fri, 26 Dec 2025 14:56:01 +0100 Subject: [PATCH 1/3] demo server uses TLS --- demo/target_server.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/demo/target_server.py b/demo/target_server.py index 73320e6..5d1333c 100644 --- a/demo/target_server.py +++ b/demo/target_server.py @@ -1,4 +1,8 @@ import socket +import ssl + +CERT_FILE = 'certs/server.crt' +KEY_FILE = 'certs/server.key' def start_target_server(host='0.0.0.0', port=80): @@ -6,6 +10,8 @@ def start_target_server(host='0.0.0.0', port=80): Simple target server that logs the client IP to demonstrate anonymity. """ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + ctx.load_cert_chain(certfile=CERT_FILE, keyfile=KEY_FILE) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind((host, port)) s.listen() @@ -21,8 +27,9 @@ def start_target_server(host='0.0.0.0', port=80): print("[TARGET] ALERT: Incoming connection established!") print(f"[TARGET] SOURCE IP (REMOTE_ADDR): {addr[0]}") print(f"[TARGET] SOURCE PORT: {addr[1]}") + sec_conn = ctx.wrap_socket(conn, server_side=True) - data = conn.recv(2048) + data = sec_conn.recv(2048) if data: decoded_data = data.decode('utf-8', errors='ignore') @@ -41,7 +48,7 @@ def start_target_server(host='0.0.0.0', port=80): "\r\n" f"{response_body}" ) - conn.sendall(response.encode('utf-8')) + sec_conn.sendall(response.encode('utf-8')) print("[TARGET] Response sent successfully.") print("[TARGET] Connection closed.") From 6f07d41213dfd420bfcdd46e91742209390e8062 Mon Sep 17 00:00:00 2001 From: wkukielk Date: Sat, 27 Dec 2025 13:54:22 +0100 Subject: [PATCH 2/3] works, no TLS between node and client --- demo/client_app.py | 23 ++++++++++++++++------- demo/target_server.py | 5 +++-- deployments/docker-compose.yml | 8 +++----- deployments/server.Dockerfile | 6 ++++++ docs/TESTING.md | 2 ++ src/minitor/socket.py | 3 ++- src/node/handler.py | 16 ++++++++++++---- 7 files changed, 44 insertions(+), 19 deletions(-) create mode 100644 deployments/server.Dockerfile diff --git a/demo/client_app.py b/demo/client_app.py index 5b1e938..faab528 100644 --- a/demo/client_app.py +++ b/demo/client_app.py @@ -3,13 +3,16 @@ This script shows that the server sees the Node's IP, not the Client's. """ from src.minitor.socket import MiniTorSocket +import ssl +from time import sleep def run_demo(): # 1. Configuration for the Proxy Node NODE_HOST = 'proxy-node.local' NODE_PORT = 8080 - CA_CERT = 'certs/node.crt' + NODE_CERT = 'certs/node.crt' + SERVER_CERT = 'demo/certs/server.crt' # 2. Target destination (The server we want to reach anonymously) TARGET_HOST = 'target-server.com' @@ -19,15 +22,21 @@ def run_demo(): print(f"[CLIENT] Proxy Node: {NODE_HOST}:{NODE_PORT}") print(f"[CLIENT] Target: {TARGET_HOST}:{TARGET_PORT}") - secure_socket = MiniTorSocket(NODE_HOST, NODE_PORT, CA_CERT) + proxy_socket = MiniTorSocket(NODE_HOST, NODE_PORT, NODE_CERT) print("[CLIENT] MiniTorSocket created") try: # Step 1: Connect to the target via the proxy node print("[CLIENT] Connecting to target through proxy node...") - secure_socket.connect(TARGET_HOST, TARGET_PORT) + proxy_socket.connect(TARGET_HOST, TARGET_PORT) print(f"[CLIENT] Connected to {TARGET_HOST} through {NODE_HOST}") + s = proxy_socket.sock + ctx = ssl.create_default_context(cafile=SERVER_CERT) + secure_socket = ctx.wrap_socket(s, server_hostname=TARGET_HOST) + + print("socket wrapped") + # Step 2: Prepare a simple HTTP request http_request = ( f"GET / HTTP/1.1\r\n" @@ -44,7 +53,7 @@ def run_demo(): # Step 4: Receive response in chunks print("[CLIENT] Waiting for response...") full_response = b"" - secure_socket.sock.settimeout(3.0) + secure_socket.settimeout(3.0) try: while True: @@ -66,11 +75,11 @@ def run_demo(): else: print("[CLIENT] No data received.") - except Exception as e: - print(f"[CLIENT] Demo failed: {e}") + # except Exception as e: + # print(f"[CLIENT] Demo failed: {e}") finally: print("[CLIENT] Closing connection") - secure_socket.close() + proxy_socket.close() if __name__ == "__main__": diff --git a/demo/target_server.py b/demo/target_server.py index 5d1333c..f28174b 100644 --- a/demo/target_server.py +++ b/demo/target_server.py @@ -1,8 +1,9 @@ import socket import ssl -CERT_FILE = 'certs/server.crt' -KEY_FILE = 'certs/server.key' + +CERT_FILE = 'demo/certs/server.crt' +KEY_FILE = 'demo/certs/server.key' def start_target_server(host='0.0.0.0', port=80): diff --git a/deployments/docker-compose.yml b/deployments/docker-compose.yml index 5151398..cdfb36e 100644 --- a/deployments/docker-compose.yml +++ b/deployments/docker-compose.yml @@ -1,14 +1,12 @@ services: # The final destination target-server: - image: python:3.11-slim + build: + context: .. + dockerfile: deployments/server.Dockerfile container_name: target-server.com environment: - PYTHONUNBUFFERED=1 - volumes: - - ../demo/target_server.py:/app/target_server.py - working_dir: /app - command: python3 target_server.py networks: - minitor-net diff --git a/deployments/server.Dockerfile b/deployments/server.Dockerfile new file mode 100644 index 0000000..3501bd3 --- /dev/null +++ b/deployments/server.Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.11-slim +WORKDIR /app +COPY src/ /app/src/ +COPY demo/ /app/demo/ +CMD ["python3", "demo/target_server.py"] +# CMD ["tail", "-f", "/dev/null"] diff --git a/docs/TESTING.md b/docs/TESTING.md index f282a16..6c67818 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -25,6 +25,8 @@ Launch the Client, Node, and Target Server. ```bash + +cd deployments docker-compose up --build ``` diff --git a/src/minitor/socket.py b/src/minitor/socket.py index bd4d453..84f0145 100644 --- a/src/minitor/socket.py +++ b/src/minitor/socket.py @@ -24,7 +24,8 @@ def connect(self, target_host: str, target_port: int): # 1 & 2: TCP + TLS sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(self.node_addr) - self.sock = self.context.wrap_socket(sock, server_hostname=self.node_addr[0]) + # self.sock = self.context.wrap_socket(sock, server_hostname=self.node_addr[0]) + self.sock = sock # 3: Send CONNECT request connect_request = ProtocolHandler.create_connect_request(target_host, target_port) diff --git a/src/node/handler.py b/src/node/handler.py index fb3e1b5..6d1df54 100644 --- a/src/node/handler.py +++ b/src/node/handler.py @@ -2,6 +2,7 @@ import threading from .shaper import TrafficShaper from ..shared.protocol import ProtocolHandler +import ssl class ConnectionHandler: @@ -10,6 +11,7 @@ class ConnectionHandler: """ def __init__(self, client_sock, ssl_context): self.client_sock = client_sock + ssl_context.options |= ssl.OP_NO_RENEGOTIATION self.ssl_context = ssl_context self.target_sock = None self.shaper = TrafficShaper() @@ -18,7 +20,8 @@ def __init__(self, client_sock, ssl_context): def run(self): try: # 1. Wrap client in TLS - secure_client = self.ssl_context.wrap_socket(self.client_sock, server_side=True) + # secure_client = self.ssl_context.wrap_socket(self.client_sock, server_side=True) + secure_client = self.client_sock # 2. Identify target using the shared protocol request_data = secure_client.recv(1024) @@ -40,7 +43,7 @@ def run(self): # Thread A: Client -> Target (Upload) up_thread = threading.Thread(target=self._relay, args=(secure_client, self.target_sock, False)) # Thread B: Target -> Client (Download with Obfuscation) - down_thread = threading.Thread(target=self._relay, args=(self.target_sock, secure_client, True)) + down_thread = threading.Thread(target=self._relay, args=(self.target_sock, secure_client, False)) up_thread.start() down_thread.start() @@ -56,20 +59,25 @@ def run(self): def _relay(self, source, destination, use_shaper): """Generic relay function for one-way traffic.""" + is_cl = source == self.target_sock + print(f"{'Target -> Client' if is_cl else 'Client -> Target'}") try: while self.running: data = source.recv(4096) if not data: + print("[Node] No data") break - + print(data.decode('ascii', errors='ignore')) if use_shaper: # Apply random delays and segmentation self.shaper.send_obfuscated(destination, data) else: destination.sendall(data) - except Exception: + except Exception as e: + print(e) pass finally: + print(f"{'target 'if is_cl else 'client'} done") self.running = False # Signal the other thread to stop def _cleanup(self): From 2bd9d1b0bd7d0a7334cc67624df77cd91d2a203c Mon Sep 17 00:00:00 2001 From: wkukielk Date: Sun, 28 Dec 2025 11:26:21 +0100 Subject: [PATCH 3/3] full TLS --- demo/client_app.py | 16 +++--- demo/target_server.py | 50 +++++++++--------- deployments/node.Dockerfile | 2 +- deployments/server.Dockerfile | 1 - src/minitor/double_socket.py | 38 ++++++++++++++ src/minitor/socket.py | 3 +- src/node/handler.py | 95 +++++++++++++++++++++++------------ 7 files changed, 133 insertions(+), 72 deletions(-) create mode 100644 src/minitor/double_socket.py diff --git a/demo/client_app.py b/demo/client_app.py index faab528..24948cb 100644 --- a/demo/client_app.py +++ b/demo/client_app.py @@ -4,7 +4,7 @@ """ from src.minitor.socket import MiniTorSocket import ssl -from time import sleep +from src.minitor.double_socket import DoubleSocket def run_demo(): @@ -31,9 +31,8 @@ def run_demo(): proxy_socket.connect(TARGET_HOST, TARGET_PORT) print(f"[CLIENT] Connected to {TARGET_HOST} through {NODE_HOST}") - s = proxy_socket.sock ctx = ssl.create_default_context(cafile=SERVER_CERT) - secure_socket = ctx.wrap_socket(s, server_hostname=TARGET_HOST) + sock = DoubleSocket(proxy_socket.sock, ctx, TARGET_HOST) print("socket wrapped") @@ -48,22 +47,21 @@ def run_demo(): # Step 3: Send the data through the "safe" socket print("[CLIENT] Sending HTTP request...") - secure_socket.send(http_request.encode('utf-8')) + sock.send(http_request.encode('utf-8')) # Step 4: Receive response in chunks print("[CLIENT] Waiting for response...") full_response = b"" - secure_socket.settimeout(3.0) + sock.sock.settimeout(5) try: while True: - chunk = secure_socket.recv(4096) + chunk = sock.recv(4096) if not chunk: print("[CLIENT] Connection closed by remote host.") break full_response += chunk print(f"[CLIENT] ... received {len(chunk)} bytes") - except Exception as e: print(f"[CLIENT] Stopping reception: {e}") @@ -75,8 +73,8 @@ def run_demo(): else: print("[CLIENT] No data received.") - # except Exception as e: - # print(f"[CLIENT] Demo failed: {e}") + except Exception as e: + print(f"[CLIENT] Demo failed: {e}") finally: print("[CLIENT] Closing connection") proxy_socket.close() diff --git a/demo/target_server.py b/demo/target_server.py index f28174b..fc1c016 100644 --- a/demo/target_server.py +++ b/demo/target_server.py @@ -28,32 +28,30 @@ def start_target_server(host='0.0.0.0', port=80): print("[TARGET] ALERT: Incoming connection established!") print(f"[TARGET] SOURCE IP (REMOTE_ADDR): {addr[0]}") print(f"[TARGET] SOURCE PORT: {addr[1]}") - sec_conn = ctx.wrap_socket(conn, server_side=True) - - data = sec_conn.recv(2048) - if data: - decoded_data = data.decode('utf-8', errors='ignore') - - print("[TARGET] Received Data Stream:") - print("-" * 40) - print(decoded_data.strip()) - print("-" * 40) - - response_body = "Hello from the Target Server! Your identity is hidden." - response = ( - "HTTP/1.1 200 OK\r\n" - "Content-Type: text/plain\r\n" - f"Content-Length: {len(response_body)}\r\n" - "Server: mini-TOR-Mock-Server\r\n" - "Connection: close\r\n" - "\r\n" - f"{response_body}" - ) - sec_conn.sendall(response.encode('utf-8')) - print("[TARGET] Response sent successfully.") - - print("[TARGET] Connection closed.") - print("-" * 50) + with ctx.wrap_socket(conn, server_side=True) as sec_conn: + data = sec_conn.recv(2048) + if data: + decoded_data = data.decode('utf-8', errors='ignore') + + print("[TARGET] Received Data Stream:") + print("-" * 40) + print(decoded_data.strip()) + print("-" * 40) + + response_body = "Hello from the Target Server! Your identity is hidden." + response = ( + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/plain\r\n" + f"Content-Length: {len(response_body)}\r\n" + "Server: mini-TOR-Mock-Server\r\n" + "Connection: close\r\n" + "\r\n" + f"{response_body}" + ) + sec_conn.sendall(response.encode('utf-8')) + print("[TARGET] Response sent successfully.") + print("[TARGET] Connection closed.") + print("-" * 50) if __name__ == "__main__": diff --git a/deployments/node.Dockerfile b/deployments/node.Dockerfile index dcfdd34..df6a76f 100644 --- a/deployments/node.Dockerfile +++ b/deployments/node.Dockerfile @@ -2,4 +2,4 @@ FROM python:3.11-slim WORKDIR /app COPY src/ /app/src/ RUN mkdir -p /app/certs -CMD ["python3", "-m", "src.node.main"] \ No newline at end of file +CMD ["python3", "-m", "src.node.main"] diff --git a/deployments/server.Dockerfile b/deployments/server.Dockerfile index 3501bd3..acc7711 100644 --- a/deployments/server.Dockerfile +++ b/deployments/server.Dockerfile @@ -3,4 +3,3 @@ WORKDIR /app COPY src/ /app/src/ COPY demo/ /app/demo/ CMD ["python3", "demo/target_server.py"] -# CMD ["tail", "-f", "/dev/null"] diff --git a/src/minitor/double_socket.py b/src/minitor/double_socket.py new file mode 100644 index 0000000..54b5da3 --- /dev/null +++ b/src/minitor/double_socket.py @@ -0,0 +1,38 @@ +import ssl + + +class DoubleSocket: + def __init__(self, proxy_sock, ctx, server_hostname): + self.sock = proxy_sock + self._incoming = ssl.MemoryBIO() + self._outgoing = ssl.MemoryBIO() + self._obj = ctx.wrap_bio(self._incoming, self._outgoing, server_hostname=server_hostname) + self._handshake() + + def _handshake(self): + while True: + try: + self._obj.do_handshake() + break + except ssl.SSLWantReadError: + data_to_send = self._outgoing.read() + if data_to_send: + self.sock.sendall(data_to_send) + response = self.sock.recv(4096) + self._incoming.write(response) + + def send(self, data): + self._obj.write(data) + encrypted = self._outgoing.read() + self.sock.send(encrypted) + + def recv(self, n): + while True: + try: + encrypted = self.sock.recv(n) + if not encrypted: + return encrypted + self._incoming.write(encrypted) + return self._obj.read() + except ssl.SSLWantReadError: + pass diff --git a/src/minitor/socket.py b/src/minitor/socket.py index 84f0145..bd4d453 100644 --- a/src/minitor/socket.py +++ b/src/minitor/socket.py @@ -24,8 +24,7 @@ def connect(self, target_host: str, target_port: int): # 1 & 2: TCP + TLS sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(self.node_addr) - # self.sock = self.context.wrap_socket(sock, server_hostname=self.node_addr[0]) - self.sock = sock + self.sock = self.context.wrap_socket(sock, server_hostname=self.node_addr[0]) # 3: Send CONNECT request connect_request = ProtocolHandler.create_connect_request(target_host, target_port) diff --git a/src/node/handler.py b/src/node/handler.py index 6d1df54..a2b0650 100644 --- a/src/node/handler.py +++ b/src/node/handler.py @@ -2,7 +2,19 @@ import threading from .shaper import TrafficShaper from ..shared.protocol import ProtocolHandler -import ssl +import os +import selectors + + +class Selector: + def __init__(self, sel, sock, rsignaler, wsignaler): + self.sel = sel + self.sock = sock + self.rsignaler = rsignaler + self.wsignaler = wsignaler + + def signal(self): + os.write(self.wsignaler, b'x') class ConnectionHandler: @@ -11,7 +23,6 @@ class ConnectionHandler: """ def __init__(self, client_sock, ssl_context): self.client_sock = client_sock - ssl_context.options |= ssl.OP_NO_RENEGOTIATION self.ssl_context = ssl_context self.target_sock = None self.shaper = TrafficShaper() @@ -20,30 +31,31 @@ def __init__(self, client_sock, ssl_context): def run(self): try: # 1. Wrap client in TLS - # secure_client = self.ssl_context.wrap_socket(self.client_sock, server_side=True) - secure_client = self.client_sock + self.client_sock = self.ssl_context.wrap_socket(self.client_sock, server_side=True) # 2. Identify target using the shared protocol - request_data = secure_client.recv(1024) + request_data = self.client_sock.recv(1024) target = ProtocolHandler.parse_request(request_data) if not target: - secure_client.sendall(b"ERROR\n") + self.client_sock.sendall(b"ERROR\n") return # 3. Connect to original destination try: self.target_sock = socket.create_connection(target, timeout=10.0) - secure_client.sendall(b"OK\n") + self.client_sock.sendall(b"OK\n") except socket.error: - secure_client.sendall(b"ERROR\n") # Notify client of failure + self.client_sock.sendall(b"ERROR\n") # Notify client of failure return + self.client_sel, self.target_sel = self._create_selectors(self.client_sock, self.target_sock) + # 4. Multithreaded Bidirectional Relay # Thread A: Client -> Target (Upload) - up_thread = threading.Thread(target=self._relay, args=(secure_client, self.target_sock, False)) + up_thread = threading.Thread(target=self._relay, args=(self.client_sel, self.target_sock, False)) # Thread B: Target -> Client (Download with Obfuscation) - down_thread = threading.Thread(target=self._relay, args=(self.target_sock, secure_client, False)) + down_thread = threading.Thread(target=self._relay, args=(self.target_sel, self.client_sock, True)) up_thread.start() down_thread.start() @@ -57,34 +69,51 @@ def run(self): finally: self._cleanup() - def _relay(self, source, destination, use_shaper): + def _relay(self, sel: Selector, destination, use_shaper): """Generic relay function for one-way traffic.""" - is_cl = source == self.target_sock - print(f"{'Target -> Client' if is_cl else 'Client -> Target'}") try: while self.running: - data = source.recv(4096) - if not data: - print("[Node] No data") - break - print(data.decode('ascii', errors='ignore')) - if use_shaper: - # Apply random delays and segmentation - self.shaper.send_obfuscated(destination, data) - else: - destination.sendall(data) - except Exception as e: - print(e) + events = sel.sel.select() + for key, mask in events: + if key.data == "SOCKET": + data = sel.sock.recv(4096) + if not data: + self._stop() + self._send(destination, data, use_shaper) + except Exception: pass finally: - print(f"{'target 'if is_cl else 'client'} done") - self.running = False # Signal the other thread to stop + self._stop() + + def _send(self, destination, data, use_shaper): + if use_shaper: + # Apply random delays and segmentation + self.shaper.send_obfuscated(destination, data) + else: + destination.sendall(data) + + def _stop(self): + self.running = False + os.write(self.client_sel.wsignaler, b'x') def _cleanup(self): """Ensures both connections are closed on error or completion.""" - if self.target_sock: - self.target_sock.close() - try: - self.client_sock.close() - except Exception: - pass + self.target_sock.close() + self.client_sock.close() + os.close(self.client_sel.rsignaler) + os.close(self.client_sel.wsignaler) + + @classmethod + def _create_selectors(cls, sock_client, sock_target): + rsignaler, wsignaler = os.pipe() + + sel_client = selectors.DefaultSelector() + sel_client.register(sock_client, selectors.EVENT_READ, data="SOCKET") + sel_client.register(rsignaler, selectors.EVENT_READ, data="ABORT") + + sel_target = selectors.DefaultSelector() + sel_target.register(sock_target, selectors.EVENT_READ, data="SOCKET") + sel_target.register(rsignaler, selectors.EVENT_READ, data="ABORT") + + return Selector(sel_client, sock_client, rsignaler, wsignaler), \ + Selector(sel_target, sock_target, rsignaler, wsignaler)