Repository: https://github.com/x0prc/AnR2
Building Anonymous Routing with Garlic-Inspired Encryption
Understanding the Architecture
AnR2 follows a layered architecture with three primary components: the frontend React application, the Flask backend with WebSocket support, and the node-based routing network. Let’s examine how these components work together.
The Node Architecture
At the heart of AnR2 lies our node-based routing system. We designed three distinct node types, each serving a specific purpose in the message routing chain:
class EntryNode:
def __init__(self):
self.connected_clients = []
def accept_connection(self, client):
self.connected_clients.append(client)
# Forward the encrypted message to the next node
self.forward_message(client.encrypted_message, next_node)
class RelayNode:
def forward_message(self, encrypted_message, next_node):
# Forward the encrypted message to the next node
next_node.receive_message(encrypted_message)
class ExitNode:
def receive_message(self, encrypted_message):
# Decrypt the message
decrypted_message = self.decrypt(encrypted_message)
# Forward the message to the final destination
self.send_to_destination(decrypted_message)Explanation: The EntryNode serves as the gateway for client connections, accepting encrypted messages and initiating the routing chain. The RelayNode acts as an intermediary, forwarding messages without decrypting them—crucial for maintaining sender anonymity. Finally, the ExitNode performs the final decryption and delivers the message to its destination. This three-tier architecture mirrors the structure of anonymous networks while remaining conceptually simple.
The Encryption Layer: AES in CBC Mode
Encryption is the foundation of any anonymous routing system. We implemented AES encryption in CBC (Cipher Block Chaining) mode to ensure message confidentiality at each routing hop.
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import os
class AESEncryption:
def __init__(self, key):
self.key = key
def encrypt(self, plain_text):
cipher = AES.new(self.key, AES.MODE_CBC)
ct_bytes = cipher.encrypt(pad(plain_text.encode(), AES.block_size))
return cipher.iv + ct_bytes
def decrypt(self, cipher_text):
iv = cipher_text[:AES.block_size]
ct = cipher_text[AES.block_size:]
cipher = AES.new(self.key, AES.MODE_CBC, iv)
return unpad(cipher.decrypt(ct), AES.block_size).decode()Explanation: Our AESEncryption class encapsulates the encryption logic. When initializing, we generate a 16-byte random key using os.urandom(16), ensuring each node has a unique encryption key.
During encryption:
- We create a new AES cipher in CBC mode, which automatically generates a random Initialization Vector (IV)
- The plaintext is padded to match AES block size requirements using PKCS7 padding
- We prepend the IV to the ciphertext, creating a self-contained encrypted package that can be decrypted later
During decryption:
- We extract the IV from the first 16 bytes of the ciphertext
- Using the stored key and extracted IV, we decrypt the remaining bytes
- The
unpadfunction removes the padding, revealing the original plaintext
This approach ensures that even if an attacker intercepts the message at any point in the routing chain, they cannot read its contents without the specific node’s key.
Real-Time Communication with Flask-SocketIO
Anonymous routing requires bidirectional communication between clients and nodes. We chose Flask-SocketIO to enable real-time message exchange:
from flask import Flask, request, jsonify
from flask_socketio import SocketIO, send, emit
app = Flask(__name__)
socketio = SocketIO(app)
# WebSocket event to receive messages
@socketio.on('message')
def handle_message(msg):
print(f"Received message: {msg}")
# Decrypt the received message
decrypted_message = aes.decrypt(bytes.fromhex(msg))
print(f"Decrypted message: {decrypted_message}")
# Broadcast
send(f"Decrypted message: {decrypted_message}", broadcast=True)
# Client sent encrypted messages
@socketio.on('send_encrypted')
def handle_encrypted_message(msg):
print(f"Received encrypted message: {msg}")
# Encrypt the received message and broadcast it
encrypted_message = aes.encrypt(msg)
print(f"Encrypted message: {encrypted_message.hex()}")
emit('receive_encrypted', encrypted_message.hex(), broadcast=True)
@socketio.on('connect')
def handle_connect():
print("Client connected")
send("Client connected to WebSocket server")
@socketio.on('disconnect')
def handle_disconnect():
print("Client disconnected")Explanation: Our WebSocket implementation provides multiple event handlers for different communication scenarios:
messageevent: Handles incoming messages from clients. We decrypt the message using our AES instance and broadcast the decrypted content to all connected clients.send_encryptedevent: Receives plaintext messages from clients, encrypts them server-side, and broadcasts the encrypted hex string. This simulates the encryption layer in our routing network.connect/disconnectevents: Provide lifecycle management, logging when clients join or leave the network.
The broadcast=True parameter ensures all connected clients receive updates, creating a distributed communication environment where messages propagate through the network.
The Frontend: React with Real-Time Updates
We built the user interface using React, integrating it with our WebSocket backend to provide real-time message updates:
import React, { useState, useEffect } from 'react';
import { sendMessageViaAPI } from './utils/api';
import { socket } from './utils/socket';
import MessageList from './components/MessageList';
import MessageInput from './components/MessageInput';
function App() {
const [messages, setMessages] = useState([]);
const [inputMessage, setInputMessage] = useState('');
useEffect(() => {
// Listen for encrypted messages from WebSocket
socket.on('receive_encrypted', (encryptedMessage) => {
setMessages((prevMessages) => [...prevMessages, `Encrypted: ${encryptedMessage}`]);
});
return () => {
socket.off('receive_encrypted');
};
}, []);
const handleSend = async () => {
setMessages((prevMessages) => [...prevMessages, `You: ${inputMessage}`]);
// Emit the message to the WebSocket server
socket.emit('send_encrypted', inputMessage);
// Send the message via REST API
const response = await sendMessageViaAPI(inputMessage);
setMessages((prevMessages) => [...prevMessages, `API Response: ${response.encrypted_message}`]);
setInputMessage('');
};
return (
<Container>
<Row className="justify-content-center">
<Col md={8}>
<h1 className="text-center">AnR2</h1>
<MessageList messages={messages} />
<MessageInput
inputMessage={inputMessage}
setInputMessage={setInputMessage}
handleSend={handleSend}
/>
</Col>
</Row>
</Container>
);
}Explanation: The React application manages message state and handles dual-channel communication:
useEffecthook: Establishes the WebSocket connection and listens forreceive_encryptedevents. When an encrypted message arrives from the server, we append it to our messages array, allowing users to see the encrypted payload.handleSendfunction: Implements a dual-transmission strategy:- Emits the message via WebSocket for real-time delivery
- Sends the message via REST API for acknowledgment and additional encryption
- State management: We use React’s
useStateto track both the message history and the current input, providing immediate UI feedback.
This dual-channel approach demonstrates how messages can traverse both synchronous (WebSocket) and asynchronous (REST) pathways in a distributed system.
The REST API Layer
In addition to WebSocket communication, we expose a REST API endpoint for message processing:
# API route to receive and encrypt a message
@app.route('/api/send', methods=['POST'])
def send_message():
data = request.get_json()
message = data['message']
# Encrypt the message using AES
encrypted_message = aes.encrypt(message)
# Simulate sending the message to the routing network
response = f"Message received and encrypted: {encrypted_message.hex()}"
return jsonify(response=response)Explanation: The /api/send endpoint accepts JSON payloads containing messages. Upon receiving a message:
- We extract the plaintext from the request body
- Encrypt it using our AES instance
- Return the encrypted message in hexadecimal format
This endpoint serves as an alternative entry point for clients that may not support WebSockets, ensuring broad compatibility across different client implementations.
Client-Side Encryption with Node.js
We also implemented a Node.js-based encryption utility that can be used for client-side message preparation:
const crypto = require('crypto');
function garlicEncrypt(message) {
// For simplicity, let's use AES encryption here. You can integrate your garlic routing logic
const algorithm = 'aes-256-cbc';
const key = crypto.randomBytes(32);
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(message, 'utf8', 'hex');
encrypted += cipher.final('hex');
return `${iv.toString('hex')}:${encrypted}`;
}Explanation: The garlicEncrypt function uses Node.js’s built-in crypto module to perform AES-256-CBC encryption. We:
- Generate a 32-byte random key and 16-byte IV using
crypto.randomBytes() - Create a cipher instance with the specified algorithm, key, and IV
- Encrypt the message in UTF-8 encoding, outputting hexadecimal strings
- Concatenate the IV and ciphertext with a colon delimiter for easy parsing
This client-side encryption capability allows messages to be encrypted before ever reaching the network, adding an additional layer of security and demonstrating the principle of end-to-end encryption.
Deployment and Running the System
AnR2 is containerized for easy deployment. We use Docker to package both the frontend and backend services:
# Base image
FROM python:3.9-slim
# Set working directory
WORKDIR /app
# Copy requirements and install dependencies
COPY requirements.txt .
RUN pip install -r requirements.txt
# Copy application code
COPY . .
# Expose port
EXPOSE 5000
# Run the application
CMD ["python", "backend/app.py"]To run the complete system:
# Start the Flask server
python3 backend/app.py
# Start the React frontend
npm start
# Or using Docker
docker-compose upFuture Directions
While AnR2 demonstrates the core concepts of anonymous routing, several enhancements would bring it closer to production-ready anonymous communication:
-
Full Garlic Routing: Implement true garlic routing where multiple messages are bundled together and routed through multiple nodes simultaneously, significantly improving network efficiency.
-
Dynamic Key Exchange: Implement a key exchange mechanism (such as Diffie-Hellman) to allow nodes to establish secure channels without pre-shared keys.
-
Path Selection Algorithm: Develop an intelligent path selection algorithm that considers node reputation, latency, and geographical distribution when building routing chains.
-
User Authentication: Add cryptographic identity management while preserving anonymity, potentially using zero-knowledge proofs.
Conclusion
Building AnR2 taught us valuable lessons about distributed systems, cryptography, and the trade-offs between anonymity and performance. By layering AES encryption, WebSocket communication, and a node-based routing architecture, we’ve created a foundation for anonymous message routing that can serve as a stepping stone toward more sophisticated privacy-preserving systems.
Happy routing! 🧄