Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions demo/client_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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"
Expand All @@ -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}")

Expand All @@ -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__":
Expand Down
56 changes: 31 additions & 25 deletions demo/target_server.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
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):
"""
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()
Expand All @@ -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__":
Expand Down
8 changes: 3 additions & 5 deletions deployments/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion deployments/node.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
CMD ["python3", "-m", "src.node.main"]
5 changes: 5 additions & 0 deletions deployments/server.Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
2 changes: 2 additions & 0 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ Launch the Client, Node, and Target Server.


```bash

cd deployments
docker-compose up --build

```
Expand Down
38 changes: 38 additions & 0 deletions src/minitor/double_socket.py
Original file line number Diff line number Diff line change
@@ -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
85 changes: 61 additions & 24 deletions src/node/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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()
Expand All @@ -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)