trixter — a high‑performance, runtime‑tunable TCP chaos proxy — a minimal, blazing‑fast written in Rust with Tokio. It lets you inject latency, throttle bandwidth, slice writes (to simulate small MTUs/Nagle‑like behavior), corrupt bytes in flight by injecting random bytes, randomly terminate connections, and hard‑timeout sessions – all controllable per connection via a simple REST API.
tokio-netem — a collection of Tokio AsyncRead/AsyncWrite adapters (delay, throttle, slice, terminate, shutdown, corrupt data, inject data) that power the Trixter proxy and can be used independently in tests and harnesses.
The remainder of this document dives into the proxy. For the adapter crate’s detailed guide, follow the tokio-netem link above.
A high‑performance, runtime‑tunable TCP chaos proxy — a minimal, blazing‑fast alternative to Toxiproxy written in Rust with Tokio. It lets you inject latency, throttle bandwidth, slice writes (to simulate small MTUs/Nagle‑like behavior), corrupt bytes in flight by injecting random bytes, randomly terminate connections, and hard‑timeout sessions – all controllable per connection via a simple REST API.
tokio::io::copy_bidirectional on a multi‑thread runtime;cd trixter/trixter
cargo build --release
Use any TCP server. Examples:
# Example: simple echo with netcat (macOS/Linux)
nc -lk 127.0.0.1 8181
# or Python
python3 -m socketserver -b 127.0.0.1 8181
RUST_LOG=info \
./target/release/trixter \
--listen 0.0.0.0:8080 \
--upstream 127.0.0.1:8181 \
--api 127.0.0.1:8888 \
--delay-ms 0 \
--throttle-rate-bytes 0 \
--slice-size-bytes 0 \
--corrupt-probability-rate 0.0 \
--terminate-probability-rate 0.0 \
--connection-duration-ms 0
Now connect your app/CLI to localhost:8080. The proxy forwards to 127.0.0.1:8181.
Base URL is the --api address, e.g. http://127.0.0.1:8888.
{
"conn_info": {
"id": "pN7e3y...",
"downstream": "127.0.0.1:59024",
"upstream": "127.0.0.1:8181"
},
"delay": { "secs": 2, "nanos": 500000000 },
"throttle_rate": 10240,
"slice_size": 512,
"terminate_probability_rate": 0.05,
"corrupt_probability_rate": 0.02
}
Notes:
delay serializes as a std::time::Duration object with secs/nanos fields (zeroed when the delay is disabled).id is unique per connection; use it to target a single connection.corrupt_probability_rate reports the current per-operation flip probability (0.0 when corruption is off).curl -s http://127.0.0.1:8888/health
curl -s http://127.0.0.1:8888/connections | jq
ID=$(curl -s http://127.0.0.1:8888/connections | jq -r '.[0].conn_info.id')
curl -i -X POST \
http://127.0.0.1:8888/connections/$ID/shutdown \
-H 'Content-Type: application/json' \
-d '{"reason":"test teardown"}'
curl -i -X POST \
http://127.0.0.1:8888/connections/_all/shutdown \
-H 'Content-Type: application/json' \
-d '{"reason":"test teardown"}'
curl -i -X PATCH \
http://127.0.0.1:8888/connections/$ID/delay \
-H 'Content-Type: application/json' \
-d '{"delay_ms":250}'
# Remove latency
curl -i -X PATCH \
http://127.0.0.1:8888/connections/$ID/delay \
-H 'Content-Type: application/json' \
-d '{"delay_ms":0}'
curl -i -X PATCH \
http://127.0.0.1:8888/connections/$ID/throttle \
-H 'Content-Type: application/json' \
-d '{"rate_bytes":10240}' # 10 KiB/s
curl -i -X PATCH \
http://127.0.0.1:8888/connections/$ID/slice \
-H 'Content-Type: application/json' \
-d '{"size_bytes":512}'
# Set 5% probability per read/write operation
curl -i -X PATCH \
http://127.0.0.1:8888/connections/$ID/termination \
-H 'Content-Type: application/json' \
-d '{"probability_rate":0.05}'
# Corrupt ~1% of operations
curl -i -X PATCH \
http://127.0.0.1:8888/connections/$ID/corruption \
-H 'Content-Type: application/json' \
-d '{"probability_rate":0.01}'
# Remove corruption
curl -i -X PATCH \
http://127.0.0.1:8888/connections/$ID/corruption \
-H 'Content-Type: application/json' \
-d '{"probability_rate":0.0}'
404 Not Found — bad connection ID400 Bad Request — invalid probability (outside 0.0..=1.0) for termination/corruption500 Internal Server Error — internal channel/handler error--listen <ip:port> # e.g. 0.0.0.0:8080
--upstream <ip:port> # e.g. 127.0.0.1:8181
--api <ip:port> # e.g. 127.0.0.1:8888
--delay-ms <ms> # 0 = off (default)
--throttle-rate-bytes <bytes/s> # 0 = unlimited (default)
--slice-size-bytes <bytes> # 0 = off (default)
--terminate-probability-rate <0..1> # 0.0 = off (default)
--corrupt-probability-rate <0..1> # 0.0 = off (default)
--connection-duration-ms <ms> # 0 = unlimited (default)
All of the above can be changed per connection at runtime via the REST API, except
--connection-duration-mswhich is a process‑wide default applied to new connections.
Each accepted downstream connection spawns a task that:
Wraps both sides with tunable adapters with tokio-netem:
DelayedWriter → optional latency
ThrottledWriter → bandwidth capSlicedWriter → fixed‑size write chunksTerminator → probabilistic abortsCorrupter → probabilistic random byte injectorShutdowner (downstream only) → out‑of‑band shutdown via oneshot channeltokio::io::copy_bidirectional until EOF/error/timeout.DashMap so the API can query/mutate it.# Add ~250ms latency and 64 KiB/s cap to the first active connection
ID=$(curl -s localhost:8888/connections | jq -r '.[0].conn_info.id')
curl -s -X PATCH localhost:8888/connections/$ID/delay \
-H 'Content-Type: application/json' -d '{"delay_ms":250}'
curl -s -X PATCH localhost:8888/connections/$ID/throttle \
-H 'Content-Type: application/json' -d '{"rate_bytes":65536}'
curl -s -X PATCH localhost:8888/connections/$ID/slice \
-H 'Content-Type: application/json' -d '{"size_bytes":256}'
curl -s -X PATCH localhost:8888/connections/$ID/termination \
-H 'Content-Type: application/json' -d '{"probability_rate":0.05}'
curl -s -X PATCH localhost:8888/connections/$ID/corruption \
-H 'Content-Type: application/json' -d '{"probability_rate":0.01}'
./trixter \
--listen 0.0.0.0:8080 \
--upstream 127.0.0.1:8181 \
--api 127.0.0.1:8888 \
--connection-duration-ms 5000
curl -s -X POST localhost:8888/connections/$ID/shutdown \
-H 'Content-Type: application/json' -d '{"reason":"too slow"}'
downstream/upstream pair) via GET /connections.PATCH calls.POST /connections/{id}/shutdown to free ports quickly.Tokio multi‑thread runtime; avoid heavy CPU work on the I/O threads.0 to disable.RUST_LOG=info (or debug) for visibility; turn off for max throughput.127.0.0.1).400 with { "error": "invalid probability; must be between 0.0 and 1.0" }.404.500.tokio-netem provides a toolbox of Tokio AsyncRead/AsyncWrite adapters that let you emulate
latency, throttling, slicing, terminations, forced shutdowns, data injections and data corruption without touching your application
code. Compose them around TcpStream (or any Tokio I/O type) to run realistic integration tests and
chaos experiments.
Add the crate to your Cargo.toml:
[dependencies]
tokio-netem = "0.1"
Each adapter accepts either a static configuration (plain Duration, usize, or f64) or a
dynamic handle (Arc<...>). Static setups are ideal for fixed scenarios in documentation or
smoke tests. Dynamic handles shine when you want to tweak behavior while the pipeline is running.
All examples below use a TCP client stream to emphasise end-to-end usage.
In practice the writer half is the most common place to inject chaos: outbound adapters such as
DelayedWriter, SlicedWriter, ThrottledWriter, and Terminator add near-zero overhead because
they operate on the data already in-flight and do not require extra buffering in memory the way a
reader-side shim might. Start perturbing on the write path first, then layer reader adapters if you
need full-duplex scenarios.
There are two extension traits – NetEmReadExt and NetEmWriteExt which significantly simplify usage of some adapters (not all supported, see the full documentation):
```rust,no_run use std::io; use tokio::net::TcpStream; use tokio_netem::io::NetEmWriteExt;
async fn main() -> io::Result<()> { let mut stream = TcpStream::connect("localhost:80") .await? .throttle_writes(32 * 1024) // 32 KB/s .slice_writes(16); // fragment writes Ok(()) } ```
DelayedReader & DelayedWriterAdds latency before reads or writes progress. DelayedReader delays the first byte of each buffered
burst; DelayedWriter delays every outbound write.
Static config example
```rust,no_run use std::time::Duration; use tokio::io::{self, AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::TcpStream; use tokio_netem::io::{NetEmReadExt, NetEmWriteExt};
async fn main() -> io::Result<()> { let stream = TcpStream::connect("127.0.0.1:5555").await?;
let mut stream = stream.delay_writes(Duration::from_millis(25));
let mut stream = BufReader::new(stream).delay_reads(Duration::from_millis(10));
stream.write_all(b"ping").await?; // ~25 ms pause before bytes depart
stream.flush().await?;
let mut line = String::new();
$ claude mcp add trixter \
-- python -m otcore.mcp_server <graph>