Skip to content

Repository files navigation

RPC From Scratch

A fully self-contained Remote Procedure Call (RPC) framework built on raw TCP sockets — with a secure variant that adds AES-128-CTR encryption, HMAC-SHA256 message authentication, and nonce-based replay protection. Designed to illustrate both the mechanics of RPC and the consequences of skipping security.


Features

Core RPC (core/rpc_core.py)

  • Newline-delimited JSON wire protocol over raw TCP sockets
  • RPCServer — registers Python functions as remote procedures; one thread per client
  • RPCClient — synchronous call interface with a magic proxy object that makes remote calls look local
  • Supports both positional ([a, b]) and keyword ({"a": 1}) parameter passing
  • Typed error codes: 404 METHOD_NOT_FOUND, 400 INVALID_PARAMS, 500 INTERNAL_ERROR, 408 TIMEOUT, 401 UNAUTHORIZED

Secure RPC (secure/secure_rpc.py)

Three layered protections on top of the core transport:

Layer Mechanism What it prevents
Confidentiality AES-128-CTR encryption Eavesdropping — attackers see only ciphertext
Integrity + Auth HMAC-SHA256 (constant-time) Tampering — any modification is detected
Replay protection Nonce + 30-second timestamp window Replay attacks — captured requests are rejected

Zero external dependencies for the crypto fallback — uses Python's hmac/hashlib if cryptography is unavailable.

Attack Demonstrations (attacks/attack_demo.py)

Live, runnable simulations of three classic network attacks against the insecure RPC:

  1. Eavesdropping — A transparent TCP proxy logs every JSON message, exposing credentials and data in plaintext
  2. Replay Attack — A captured auth.login request is replayed verbatim; the server accepts it without question
  3. Response Tampering — An active MITM proxy doubles all numeric results; the client has no way to detect it

Each attack is followed by a countermeasure summary pointing to the secure implementation.

Demo Services (services/demo_services.py)

19 pre-registered remote procedures across five namespaces:

Namespace Procedures
math.* add, subtract, multiply, divide, sqrt, factorial
str.* reverse, uppercase, word_count, caesar
kv.* set, get, delete, list
auth.* login, whoami
sys.* time, ping, info

Project Structure

rpc_project/
├── server.py               # Standalone server entry point (secure or insecure)
├── client.py               # Interactive REPL client
├── main.py                 # Placeholder entry point
│
├── core/
│   └── rpc_core.py         # Transport, RPCServer, RPCClient, wire protocol
│
├── secure/
│   └── secure_rpc.py       # SecureRPCServer, SecureRPCClient, crypto primitives
│
├── services/
│   └── demo_services.py    # 19 demo remote procedures (math, str, kv, auth, sys)
│
├── attacks/
│   ├── attack_demo.py      # EavesdropProxy, ReplayAttacker, TamperingProxy
│   ├── verify_attacks.py   # Automated verification of attack outcomes
│   └── QUICK_REFERENCE.md  # Attack cheat-sheet
│
└── demo/
    └── run_demo.py         # Self-contained end-to-end walkthrough

Quick Start

Prerequisites

  • Python ≥ 3.10
  • Install dependencies:
pip install -r requirements.txt
# or, with uv:
uv sync

requirements.txt lists cryptography>=41.0.0 (for AES-CTR) and colorama>=0.4.6. The fallback HMAC-based stream cipher is used automatically if cryptography is absent.


1. Start the Server

# Secure mode (default) — AES + HMAC on port 9000
python server.py

# Insecure mode (plaintext)
python server.py --insecure

# Custom host, port, and shared secret
python server.py --host 0.0.0.0 --port 9999 --key my_shared_secret

2. Connect with the Interactive Client

# In a separate terminal
python client.py

# Options
python client.py --insecure                      # Match insecure server
python client.py --host 192.168.1.5 --port 9999  # Remote server
python client.py --key my_shared_secret           # Custom key (must match server)

The REPL accepts commands of the form <method> [arg1] [arg2] ...:

  rpc(secure)> math.add 10 32
  ✓  42  (1.3 ms)

  rpc(secure)> auth.login alice password123
  ✓  {"status": "ok", "token": "alice:1700000000", "user": "alice"}  (2.1 ms)

  rpc(secure)> str.caesar "Hello World" 13
  ✓  Uryyb Jbeyq  (0.9 ms)

  rpc(secure)> sys.info
  ✓  {"os": "Darwin", "python": "3.12.0", "pid": 12345, "uptime": ...}

  rpc(secure)> list       # show all available methods
  rpc(secure)> help       # show usage examples
  rpc(secure)> quit

3. Run the Attack Demonstrations

# All three attacks against the insecure server
python -m attacks.attack_demo

# Or run the secure vs insecure comparison
python -m secure.secure_rpc

# Automated verification suite
python -m attacks.verify_attacks

4. Run the Full Demo

python -m demo.run_demo

Wire Protocol

All messages are newline-delimited JSON sent over a persistent TCP connection.

Insecure (plain)

// Request
{"id": 1, "method": "math.add", "params": [2, 3], "ts": 1700000000.0}

// Response
{"id": 1, "result": 5, "error": null, "ts": 1700000000.1}

// Error
{"id": 1, "result": null, "error": {"code": 404, "msg": "Method 'foo' not found"}, "ts": ...}

Secure (encrypted envelope)

{
  "iv":   "<hex: 16-byte AES IV>",
  "ct":   "<hex: AES-CTR ciphertext of the original JSON>",
  "hmac": "<hex: HMAC-SHA256(iv + ct, mac_key)>",
  "ts":   1700000000.0
}

The ts field is unencrypted to allow the server's replay pre-check before decryption.


Using the Library Directly

from core.rpc_core import RPCServer, RPCClient

# Server
server = RPCServer(host='0.0.0.0', port=9000)

@server.register()
def add(a, b):
    return a + b

@server.register(name='math.power')
def power(base, exp):
    return base ** exp

server.start(blocking=False)

# Client — explicit call
with RPCClient('127.0.0.1', 9000) as client:
    result = client.call('add', [10, 32])   # → 42

# Client — proxy (looks like a local function call)
with RPCClient('127.0.0.1', 9000) as client:
    result = client.proxy.add(10, 32)       # → 42

Secure variant:

from secure.secure_rpc import SecureRPCServer, SecureRPCClient

SECRET = b'my_shared_secret_32b'

server = SecureRPCServer(host='0.0.0.0', port=9100, secret_key=SECRET)

@server.register()
def greet(name):
    return f"Hello, {name}!"

server.start(blocking=False)

with SecureRPCClient('127.0.0.1', 9100, secret_key=SECRET) as client:
    print(client.proxy.greet('World'))   # Hello, World!

Security Overview

Attack                  Insecure RPC          Secure RPC
────────────────────    ──────────────────    ──────────────────
Eavesdropping           Vulnerable ✗          Protected ✓
Response Tampering      Vulnerable ✗          Detected ✓
Replay Attack           Vulnerable ✗          Rejected ✓
Credential Theft        Vulnerable ✗          Encrypted ✓

Key derivation separates encryption and MAC keys from the single shared secret:

enc_key = SHA-256(secret || "enc")[:16]   # 16-byte AES-128 key
mac_key = SHA-256(secret || "mac")        # 32-byte HMAC-SHA256 key

Requirements

Requirement Version
Python ≥ 3.10
cryptography ≥ 41.0.0 (optional — fallback available)
colorama ≥ 0.4.6

No framework dependencies. The RPC core uses only the Python standard library (socket, threading, json, hmac, hashlib).


License

This project is licensed under the MIT License

About

A simple implementation of secure RPC and demonstrations of attack on insecure RPC and secure RPC.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages