Learn how to build a full-duplex TCP proxy with real-time mode switching in Rust. This article covers the challenges of bidirectional forwarding, async I/O with Tokio, and creating an interactive terminal interface for network testing.
Elattar Saad
Thu, 27th November 20256 min read
Building network testing tools often reveals the complexity hidden beneath simple requirements. This article documents the creation of an interactive TCP proxy in Rust that allows real-time simulation of various network conditions—from latency injection to complete connection blocking.
The Problem
Testing application resilience under adverse network conditions traditionally requires either modifying production infrastructure or using complex simulation tools. What if you could simply place a proxy between your application and any service, then dynamically inject failures, latency, or bandwidth constraints?
That's the goal: a lightweight TCP proxy with an interactive menu that lets you switch between different network behaviors on the fly.
These weren't arbitrary choices. Tokio provides mature async I/O primitives, Crossterm handles terminal control across platforms, and Rust's type system prevents the subtle bugs that plague network code.
Core Components
The system consists of three concurrent components:
TCP Listener - Accepts incoming connections
Connection Handlers - One spawned per connection for bidirectional forwarding
Written by
Saad Elattar
Software engineer passionate about building scalable systems, exploring new technologies, and sharing knowledge through writing.
Interactive Menu - Runs in a blocking thread, updates shared state
All components share access to the current proxy mode through Arc<Mutex<ProxyMode>>, allowing real-time behavior changes without restarting.
The Bidirectional Challenge
First Attempt (The Trap)
The initial implementation seemed logical—read from client, forward to server, read from server, forward to client:
This code compiles. It looks reasonable. It doesn't work.
When you test it with a simple HTTP request, the connection hangs indefinitely. The client sends a request and waits for a response, but the proxy is stuck waiting to read MORE from the client instead of reading the server's response.
The Realization
This is half-duplex communication—only one direction operates at a time. TCP requires full-duplex communication where both directions operate independently and simultaneously.
Think of it like a phone call. If you could only listen OR speak (but not both at the same time), conversations would be impossible. TCP connections work the same way—data must flow in both directions concurrently.
The Solution
The fix requires splitting each connection into independent read and write halves, then running two concurrent tasks:
Now both directions run simultaneously. When a client sends an HTTP request, the client→server task forwards it immediately while the server→client task waits for the response. True bidirectional forwarding.
Proxy Modes
The proxy supports five operational modes:
Allow
Normal operation—traffic flows transparently without modification.
Deny
Blocks all incoming connections immediately.
Use case: Testing application behavior when services are completely unavailable.
Latency
Adds artificial delay to each packet in both directions.
Use case: Simulating geographic distance or slow networks (e.g., 200ms for intercontinental connections).
Timeout
Causes connections to timeout after a specified duration.
Use case: Testing retry logic and timeout handling in applications.
Throttle
Limits bandwidth by calculating required sleep time based on bytes transferred.
Use case: Simulating slow mobile connections (e.g., 10KB/s for 3G).
The Interactive Menu
Threading Challenge
The menu requires blocking I/O for keyboard input, while the proxy requires async I/O for network operations. These don't mix.
Solution: Isolate the menu in a dedicated blocking thread using tokio::task::spawn_blocking:
This prevents blocking operations from interfering with async network I/O.
Terminal Control
The menu uses Crossterm's raw mode for real-time keyboard input:
Raw mode is enabled when showing the menu and disabled when prompting for input values, preventing terminal state corruption.
Shared State Management
Multiple connection handlers need to read the current mode while the menu updates it. This requires thread-safe shared state:
Each connection handler reads the current mode before each transfer:
When the user changes the mode via the menu, all handlers immediately see the update on their next read operation. This enables dynamic behavior changes without dropping existing connections.
Signal Handling
Ctrl+C handling runs in a separate async task to ensure clean shutdown:
This works regardless of whether the user is in the menu or the proxy is handling connections.
Performance Characteristics
Memory Usage
Base overhead: Minimal (shared mode state in Arc)
Per connection: ~16KB (8KB buffer × 2 directions)
Scales linearly with concurrent connections
The 8KB buffer size balances throughput and memory usage. Larger buffers improve throughput for high-bandwidth transfers but increase memory consumption.
CPU Usage
Allow mode: Minimal overhead—mostly system calls for read/write
Latency/Throttle modes: Higher due to sleep operations
All modes: No busy-waiting—uses async sleep
Concurrency
Each connection spawns 2 async tasks. Tokio's work-stealing scheduler efficiently handles thousands of concurrent connections on modern hardware.
Limitations
Mode Application Timing
Modes apply to new read operations, not mid-transfer. If a large file transfer is in progress when you switch from Allow to Latency mode, the current transfer completes at full speed. The next read operation will apply the new latency.
Throttle Accuracy
Throttling uses sleep delays rather than token bucket or leaky bucket algorithms. This provides approximate bandwidth limiting suitable for testing but not precise rate control.
Protocol Agnostic
The proxy operates at the TCP level. It cannot inspect or modify:
HTTPS/TLS traffic (encrypted)
HTTP headers (no protocol awareness)
Application-layer protocols
This is both a limitation and a feature—protocol independence means the proxy works with any TCP-based protocol.
Buffer-Based Operation
Uses fixed 8KB buffers. Very large messages are automatically chunked by TCP, but this isn't optimized for any specific protocol's message boundaries.
Testing Approach
The proxy was validated using:
HTTP traffic: curl requests through the proxy to various web servers
Database connections: PostgreSQL client connections via the proxy
Long-lived connections: WebSocket connections to verify bidirectional handling
Mode transitions: Changing modes during active transfers
Concurrent load: Multiple simultaneous connections under different modes
Use Cases
Testing Service Resilience
Simulating Geographic Distance
Debugging Timeout Issues
Bandwidth Constraint Testing
Implementation Details
The complete implementation fits in approximately 250 lines of Rust code:
Key Insights
The critical insight was recognizing that TCP proxying requires truly concurrent bidirectional forwarding, not sequential request-response handling. The initial half-duplex implementation would work for simple ping-pong protocols but fails for real-world TCP traffic patterns.
The interactive menu approach, while requiring careful thread management between blocking and async contexts, provides significant usability benefits over configuration files when manually testing application resilience.
Conclusion
Building this proxy revealed how seemingly simple requirements—"forward TCP traffic"—hide significant complexity. The bidirectional forwarding challenge demonstrates why understanding the underlying protocol semantics matters more than writing code that compiles.
The result is a practical tool for testing network resilience that runs anywhere Rust does, requires no configuration, and lets you inject failures interactively. Whether you're testing timeout handling, simulating geographic latency, or practicing chaos engineering, having a programmable proxy in your toolkit makes these tasks straightforward.