Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

History

2 Commits

Repository files navigation

async-proxy

A multi-threaded async reverse proxy built in Rust. Forwards HTTP and tunnels HTTPS, enforces per-IP rate limiting, rewrites headers, and logs every request with structured output.

2026-05-02T15:43:50.358Z INFO → request method="GET" path="/get" status=200 latency="473ms"
2026-05-02T15:43:56.473Z WARN rate limited ip=192.168.1.5
2026-05-02T15:43:56.473Z INFO → request method="GET" path="/get" status=429 latency="0ms"
2026-05-02T15:43:52.221Z INFO CONNECT tunnel target=github.com:443 client=127.0.0.1:56012

Why I built this

I wanted to understand what actually happens inside a reverse proxy — how async runtimes handle thousands of concurrent connections without threads, how HTTP is modelled as a function from request to response, and how HTTPS passthrough works without decrypting anything. Building it from scratch in Rust forced me to reason about ownership across async task boundaries, shared mutable state without data races, and the difference between a TCP connection and an HTTP request.


Features

  • Async from the ground up — built on Tokio; each TCP connection gets its own spawned task, no blocking anywhere
  • HTTP forwarding — proxies plain HTTP requests to any upstream, preserving the full response
  • HTTPS CONNECT tunnel — handles the CONNECT method by opening a raw TCP tunnel, letting the client negotiate TLS directly with the upstream (no decryption)
  • Per-IP rate limiting — token bucket algorithm, one bucket per client IP stored in a DashMap; burst capacity + continuous refill rate, both configurable
  • Header rewriting — strips hop-by-hop headers (Proxy-Connection, Transfer-Encoding, etc.) and injects X-Forwarded-For, X-Real-IP, and Via
  • Structured logging — every request logged with method, path, status code, and latency via tracing; rate-limited requests log at WARN level
  • Zero-cost rejection — rate-limited requests are rejected entirely in memory at 0ms latency, no upstream I/O

Architecture

Client
│ TCP connection
▼
TcpListener::accept().await
│ tokio::spawn (one task per connection)
▼
service_fn(|req| ...) ← called once per HTTP request
│
├─ Rate limiter check token bucket for client IP
│ └─ 429 if empty rejected in memory, 0ms latency
│
├─ Header rewriter strip hop-by-hop, inject X-Forwarded-For
│
├─ Request logger method · path · status · latency
│
├─ HTTP forward ──────────► TcpStream::connect(upstream)
│ hyper::client::conn::http1
│
└─ CONNECT tunnel ─────────► tokio::io::copy_bidirectional
raw bytes, no TLS inspection

Token bucket rate limiter

 capacity = 10 tokens
refill = 0.5 tokens/sec
┌──────────────────────┐
│ ● ● ● ● ● ● ● ● ● ● │ full bucket → requests allowed
└──────────────────────┘
↓ 10 requests
┌──────────────────────┐
│ │ empty → 429 Too Many Requests
└──────────────────────┘
↓ wait 20s (refill)
┌──────────────────────┐
│ ● ● ● ● ● ● ● ● ● ● │ full again
└──────────────────────┘

Each IP gets its own independent bucket. DashMap gives per-bucket locking — no global mutex contention under concurrent load.


Getting started

Prerequisites: Rust 1.75+ (install)

git clone https://github.com/YOUR_USERNAME/async-proxy.git
cd async-proxy
cargo build --release

Running

cargo run
# 2026-05-02T15:43:39Z INFO async-proxy listening addr=127.0.0.1:8080 burst=10 refill=0.5

Test HTTP forwarding

curl -x http://127.0.0.1:8080 http://httpbin.org/get

Test header injection

# X-Forwarded-For is sent upstream — visible in the origin field
curl -x http://127.0.0.1:8080 http://httpbin.org/get
# "origin": "127.0.0.1, <your-public-ip>"

Test HTTPS tunnel

# curl uses CONNECT automatically for https:// targets
curl -x http://127.0.0.1:8080 https://httpbin.org/get

Test rate limiting

# First 10 → 200, then 429 until the bucket refillsforiin$(seq 1 15);doecho -n "req $i: "
curl -s -o /dev/null -w "%{http_code}\n" -x http://127.0.0.1:8080 http://httpbin.org/get
done# Wait for bucket to refill (20s at 0.5 tokens/sec)
sleep 20 && curl -x http://127.0.0.1:8080 http://httpbin.org/get

Configuration

All tunable constants are at the top of src/main.rs:

ConstantDefaultDescription
LISTEN_ADDR127.0.0.1:8080Address and port to bind
RATE_LIMIT_BURST10Max burst requests before throttling
RATE_LIMIT_REFILL0.5Tokens refilled per second per IP

Project structure

src/
main.rs — Tokio runtime, TCP accept loop, connection dispatch
proxy.rs — HTTP forwarding + CONNECT tunnel handler
middleware.rs — Header rewriter and request logger
rate_limiter.rs — Token bucket, one per IP via DashMap

Crates used

CratePurpose
tokioAsync runtime, TCP listener, task spawning, bidirectional copy
hyperHTTP/1.1 server and client, CONNECT upgrade
hyper-utilTokioIo adapter between Tokio and Hyper I/O traits
http-body-utilBody boxing and composition
dashmapConcurrent hash map for per-IP rate limit buckets
tracingStructured, async-aware logging
tracing-subscriberLog formatting and RUST_LOG env filter
bytesZero-copy byte buffer for response bodies
anyhowErgonomic error handling in main

License

MIT

About

Async reverse proxy in Rust — Tokio + Hyper, per-IP token bucket rate limiting, X-Forwarded-For rewriting, HTTPS CONNECT tunnel, structured tracing logs

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages