P2P Reverse Proxy
File: kinetic-host/src/proxy.rs
Crate: kinetic-host | Stage: 13
What Is This?
The reverse proxy is what makes the host actually serve content. When a Kinetic user’s daemon makes a P2P ProxyRequest to saif.kin, that request travels over libp2p to this host’s running process. proxy.rs receives it, translates it into a standard HTTP request to the local web server, and routes the HTTP response back over P2P.
This is the “server side” of the proxy pair — kinetic-daemon/src/proxy/ is the “client side” that sends requests.
How It Works
handle_incoming_proxy_requests — the main loop
-> See: kinetic-host/src/proxy.rs — Lines 83–149
Receives (ProxyRequest, ResponseChannel<ProxyResponse>) tuples from the incoming_rx mpsc channel. For each request, spawns a new Tokio task so multiple requests are handled concurrently (not sequentially).
Path validation (before any HTTP call):
#![allow(unused)]
fn main() {
let decoded_path = percent_encoding::percent_decode_str(&req.path)
.decode_utf8()
.unwrap_or(...);
if decoded_path.contains("..") || !decoded_path.starts_with('/') {
// Return 400 Bad Request
}
}
Warning
Percent-decodes the path first (to catch
%2E%2Eas..), then checks for path traversal attempts. Paths not starting with/are also rejected.
Body size check:
#![allow(unused)]
fn main() {
if req.body.len() > LIMITS_PROXY_MAX_BODY_BYTES {
// Return 413 Payload Too Large
}
}
Important
Checked before calling
forward_request. Prevents the local backend from receiving oversized P2P payloads.
forward_request — HTTP translation
-> See: kinetic-host/src/proxy.rs — Lines 10–76
Constructs a reqwest HTTP request from the ProxyRequest:
- Parses
req.methodas an HTTP method, defaults toGETif unrecognized. - Copies all headers from the P2P request except
Host(stripped and replaced with the local backend’shost:port). Without stripping, the backend would seeHost: saif.kinand reject the request. - Sets
req.bodyas the HTTP body.
Response body streaming with size cap:
#![allow(unused)]
fn main() {
let mut stream = res.bytes_stream();
while let Some(chunk_res) = stream.next().await {
body.extend_from_slice(&chunk);
if body.len() > LIMITS_PROXY_MAX_BODY_BYTES {
// Truncate, set status = 502
break;
}
}
}
The backend response is streamed in chunks. If the total exceeds LIMITS_PROXY_MAX_BODY_BYTES (5MB), the body is discarded and a 502 is returned. This prevents the host from relaying gigabyte responses over P2P.
Error → 502: If reqwest fails to connect to the backend at all (backend not running), returns 502 Bad Gateway with an error message including the failed port number.
reqwest::Client configuration
#![allow(unused)]
fn main() {
reqwest::Client::builder().no_proxy().build().unwrap_or_default()
}
Note
.no_proxy()prevents the HTTP client from accidentally routing through the system proxy (which on a host machine might be Kinetic’s own PAC). Without this, you’d get infinite loops.
Tests
proxy_handles_chaotic_requests_gracefully (proptest):
-> See: kinetic-host/src/proxy.rs — Lines 157–179
Generates random HTTP methods, paths, and bodies and calls forward_request() pointing at port 65534 (guaranteed dead). Asserts:
- Never panics.
- Always returns
502(since the backend is dead). - Response body contains
"Bad Gateway".
Full integration tests are in proxy_tests.rs (a separate file, 427 lines) — those spin up a real Axum backend server, send real P2P-format requests through forward_request, and assert the correct HTTP response is mirrored back.
Quick Reference
| Check | Where | Response on Fail |
|---|---|---|
Path traversal (..) | Before HTTP call | 400 Bad Request |
Path doesn’t start with / | Before HTTP call | 400 Bad Request |
| Request body too large | Before HTTP call | 413 Payload Too Large |
| Backend unreachable | In forward_request | 502 Bad Gateway |
| Backend response too large | During streaming | 502 Payload Too Large |
Host header from P2P | In forward_request | Stripped + replaced |