Privacy-focused Ethereum JSON-RPC proxy served as a Tor hidden service, with optional MEV-protected sends via Flashbots.
There are two binaries in this workspace:
torpc— the daemon. Runs alongside a Geth node, exposes a Tor hidden service that filters JSON-RPC, rate-limits, and optionally signs bundles for Flashbots. Operators run this.torpc-proxy— the local client proxy. A wallet user runs this on their own machine; it listens on127.0.0.1:8545, forwards each request through Tor to the configured.onion, and returns the response.
Status: working but not yet release-distributed. Build from source.
| Role | Goal | Path |
|---|---|---|
| Operator | Run a Tor-fronted RPC, optionally protect sends via Flashbots | Operator Setup |
| Wallet user | Route a wallet through someone else's TorPC .onion |
Wallet User Setup |
| Developer | Hack on TorPC | Development |
- Rust 1.70+ (
rustup default stable) - Geth 1.10+ (or any Ethereum execution client speaking JSON-RPC)
- Tor 0.4.5+ (
apt install tor,brew install tor, etc.) - Linux/macOS. Windows works under WSL2.
git clone https://github.com/CPerezz/torpc.git
cd torpc
cargo build --release --bin torpcThe daemon binary lands in target/release/torpc.
The daemon reads its config from environment variables. Every var (with
defaults and effects) is documented in .env.example — copy
it to .env and edit in place; dotenvy auto-loads it at startup.
The most common knobs:
GETH_URL=http://127.0.0.1:8545 # Upstream Ethereum node
BIND_ADDR=127.0.0.1:8080 # Tor hidden service forwards here
RUST_LOG=info # Use `debug` to see the .onion address
# Optional: Flashbots MEV protection. Without a signing key, sendBundle
# returns JSON-RPC -32004 instead of silently faking a bundle hash.
# FLASHBOTS_SIGNING_KEY=<64-hex-chars-no-0x>
# FLASHBOTS_RELAY_URL=https://relay.flashbots.netThe hidden service config lives at configs/torrc. The
daemon parses it on startup and refuses to start if it disables anonymity
(HiddenServiceSingleHopMode 1 or HiddenServiceNonAnonymousMode 1) — set
TORPC_ALLOW_NON_ANONYMOUS=1 to override (CI/benchmarks only; never in
production).
# First-time setup: generate the hidden service identity
mkdir -p data/tor/torpc && chmod 700 data/tor/torpc
tor -f configs/torrc
# After Tor bootstraps (~30-60s):
cat data/tor/torpc/hostname
# 3g2upl4pq6kufc4m7v5jzvfncqvhv5bak6d2nprpz7hgbc6jvdnq5jqd.onionThe daemon does not print the .onion at INFO log level (it leaks into
syslog/log shippers). Use RUST_LOG=debug or cat the hostname file.
For local development, use the bundled scripts that start everything:
./scripts/start-all-dev.sh # Geth --dev + Tor + daemon
./scripts/stop-all.sh # Graceful shutdownFor production, ship via systemd. Templates live in
deploy/systemd-example/:
# Render and install (asks for the deploy paths):
sudo deploy/systemd-example/install-systemd.sh
sudo systemctl daemon-reload
sudo systemctl enable --now torpc-tor torpc-daemon
# Logs and status
sudo journalctl -u torpc-daemon -f
sudo systemctl status torpc-daemonThe unit ships with NoNewPrivileges, ProtectSystem=strict, PrivateTmp,
and RestrictAddressFamilies — read each .service.template before
installing.
/health and /metrics live on a separate localhost-only listener
(default 127.0.0.1:9001, set via ADMIN_BIND_ADDR). They are NOT
reachable through the Tor hidden service — the .onion forwards only to
BIND_ADDR. This is deliberate: those endpoints leak component state,
request volume, uptime, and version, none of which should be visible to
anonymous Tor visitors.
curl http://127.0.0.1:9001/metrics | jq
# {
# "security_metrics": {
# "blocked_requests_total": 0,
# "invalid_methods": 0,
# "rate_limit_hits": 0,
# ...
# },
# "circuits": { "geth": "closed", "mev_relay": "disabled" }
# }
curl http://127.0.0.1:9001/health
# { "status": "ok", "uptime_seconds": 1234, ... }For remote scraping (Prometheus, etc.), use an SSH tunnel or terminate
inside your reverse proxy with auth. The daemon refuses to bind
ADMIN_BIND_ADDR to a non-loopback address.
Heads up: there are no pre-built binaries yet. You'll need a Rust toolchain to build the client proxy.
git clone https://github.com/CPerezz/torpc.git
cd torpc
cargo build --release -p torpc-proxy-cli
# Binary at: target/release/torpc-proxyYou need:
- A running Tor daemon on your machine (
brew install tor && tor &or your distro's package — Tor Browser counts but uses port9150instead of9050). - The operator's
.onionaddress.
Create torpc-proxy.toml next to the binary (or pass --config /path/to.toml):
listen_addr = "127.0.0.1:8545"
tor_proxy = "127.0.0.1:9050" # 9150 if you're using Tor Browser
onion_endpoint = "your-operator.onion:80"./target/release/torpc-proxy start --config torpc-proxy.tomlThe proxy strips wallet-fingerprinting headers (User-Agent, Origin, Cookie,
Authorization, Sec-CH-UA-*, etc.) before forwarding through Tor, replacing
them with a synthetic User-Agent: torpc-proxy/<version>. This is the
point of the proxy — without it, your MetaMask/Coinbase/Rabby UA gives
you up on every request.
In MetaMask / Coinbase Wallet / Rabby / Trust Wallet:
- Add a custom RPC network
- RPC URL:
http://127.0.0.1:8545 - Chain ID:
1(Ethereum mainnet) — or whatever the operator's upstream is configured for - Currency Symbol:
ETH
.env.example documents every variable the daemon reads.
Highlights:
| Variable | Default | Effect |
|---|---|---|
GETH_URL |
http://127.0.0.1:8545 |
Upstream JSON-RPC |
BIND_ADDR |
127.0.0.1:8080 |
Tor-facing listener (the hidden service forwards here) |
ADMIN_BIND_ADDR |
127.0.0.1:9001 |
Localhost-only listener for /health and /metrics |
RUST_LOG |
info |
tracing-subscriber filter |
MAX_REQUEST_SIZE |
1048576 |
Body-size cap (1 MiB) |
RATE_LIMIT_REQUESTS |
100 |
Per-(IP, source-port) bucket per window |
RATE_LIMIT_WINDOW |
60 |
Window in seconds |
WRITE_RATE_LIMIT_REQUESTS |
10 |
Tighter bucket for eth_sendRawTransaction/eth_sendBundle |
MAX_CONCURRENT_CONNECTIONS |
256 |
Router-wide in-flight cap |
STRICT_SECURITY_HEADERS |
true |
When true, no CORS. Default. |
FLASHBOTS_SIGNING_KEY |
unset | Enables MEV protection when set |
FLASHBOTS_RELAY_URL |
https://relay.flashbots.net |
Mainnet by default |
TORPC_ALLOW_NON_ANONYMOUS |
unset | Override anonymity check (CI only) |
This protects against:
- Network observers (ISP, VPN provider, RPC operator) seeing wallet→RPC request contents and metadata. Tor circuits hide the user's IP.
- The upstream RPC operator fingerprinting users via wallet-specific headers (User-Agent, Origin, Sec-CH-UA-*) — the client proxy strips these.
- The upstream RPC operator linking transactions to a specific wallet via HTTP headers — same mechanism.
- Method-level abuse (rate limits + a strict allow-list of JSON-RPC methods
blocks
eth_accounts,personal_*,admin_*, etc.). - Front-running for MEV-protected sends —
eth_sendRawTransactionon/rpc/flashbotsroutes to Flashbots with EIP-191 auth.
This does NOT protect against:
- A malicious daemon operator. They see your raw JSON-RPC requests (including transaction contents). The trust boundary is the operator, same as any RPC.
- Traffic-correlation attacks against Tor itself (a global passive adversary, etc.).
- A malicious wallet (the wallet sees keys and signs txs locally).
- Smart-contract MEV (sandwich attacks at the protocol level — Flashbots helps with sequencing, not contract-level exposure).
- Side channels in the operator's environment (Geth logs, OS journal, etc.) if the operator doesn't lock those down.
The daemon goes to some lengths to avoid leaking operator identity to anonymous Tor visitors:
- No
ServerorX-Serviceresponse header - Geth error bodies (which include version strings) are not forwarded
- Per-tx routing decisions and bundle hashes are below INFO log level
- The .onion address is below INFO log level
- Strict CSP, no CORS by default
torpc/
├── src/ # Daemon
│ ├── main.rs, app.rs, proxy.rs, ...
│ └── mev/ # Flashbots auth + retry + circuit breaker
├── torpc-proxy/ # Client workspace members
│ ├── torpc-proxy-core/
│ ├── torpc-proxy-cli/
│ └── torpc-proxy-gui/ # Tauri desktop UI
├── tests/ # Daemon integration tests
├── static/ # Operator-facing wallet onboarding UI
├── configs/torrc # Tor hidden service config
├── deploy/systemd-example/ # Templated systemd units
└── scripts/ # Dev convenience scripts
make test # Fast suite, no daemons. ~140 tests in <30s.
make test-with-services # Brings up Geth + Tor + daemon, runs the whole set.CI runs make test, cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, and cargo audit (signal-only for now).
# In one terminal:
./scripts/start-all-dev.sh
# In another:
curl -X POST http://localhost:8080/rpc \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'
# Through Tor (after Tor bootstraps):
torsocks curl -X POST http://$(cat data/tor/torpc/hostname)/rpc \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'The static UI at http://localhost:8080 serves two templates depending
on the request Host header:
- Operator dashboard (
Host: 127.0.0.1:8080or any non-.onion): endpoint reference, JSON-RPC test panel, self-test wallet flows. This is what the operator sees when theycurlor browse the bind address directly. - Wallet-onboarding flow (
Host: <hostname>.onion): install-the- client step gated by a JS probe oflocalhost:8545, then a wallet picker (MetaMask / Coinbase Wallet / Rabby / Trust Wallet) with copy- to-clipboard RPC URL and per-wallet "Add network" buttons. This is what visitors see when they reach the daemon through Tor.
Source-of-truth templates are static/index_operator.html and
static/index_user.html; routing lives in src/app.rs::serve_root.
MIT — see LICENSE.
Built with 🦀 Rust and 🧅 Tor.