diff --git a/demo/client_app.py b/demo/client_app.py index 5b1e938..24948cb 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 src.minitor.double_socket import DoubleSocket 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,20 @@ 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}") + ctx = ssl.create_default_context(cafile=SERVER_CERT) + sock = DoubleSocket(proxy_socket.sock, ctx, TARGET_HOST) + + print("socket wrapped") + # Step 2: Prepare a simple HTTP request http_request = ( f"GET / HTTP/1.1\r\n" @@ -39,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.sock.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}") @@ -70,7 +77,7 @@ def run_demo(): 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 73320e6..fc1c016 100644 --- a/demo/target_server.py +++ b/demo/target_server.py @@ -1,4 +1,9 @@ import socket +import ssl + + +CERT_FILE = 'demo/certs/server.crt' +KEY_FILE = 'demo/certs/server.key' def start_target_server(host='0.0.0.0', port=80): @@ -6,6 +11,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,31 +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]}") - - data = 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}" - ) - 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/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/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 new file mode 100644 index 0000000..acc7711 --- /dev/null +++ b/deployments/server.Dockerfile @@ -0,0 +1,5 @@ +FROM python:3.11-slim +WORKDIR /app +COPY src/ /app/src/ +COPY demo/ /app/demo/ +CMD ["python3", "demo/target_server.py"] 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/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/node/handler.py b/src/node/handler.py index fb3e1b5..a2b0650 100644 --- a/src/node/handler.py +++ b/src/node/handler.py @@ -2,6 +2,19 @@ import threading from .shaper import TrafficShaper from ..shared.protocol import ProtocolHandler +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: @@ -18,29 +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) + 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, True)) + down_thread = threading.Thread(target=self._relay, args=(self.target_sel, self.client_sock, True)) up_thread.start() down_thread.start() @@ -54,29 +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.""" try: while self.running: - data = source.recv(4096) - if not data: - break - - if use_shaper: - # Apply random delays and segmentation - self.shaper.send_obfuscated(destination, data) - else: - destination.sendall(data) + 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: - 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)