A high-performance, asynchronous Modbus TCP proxy server built with Rust and Tokio. This proxy forwards Modbus TCP requests from clients to a target Modbus server with Unit-ID passthrough, providing robust connection management and comprehensive metrics.
- Unit-ID Passthrough: Forwards client Unit-IDs transparently to the target server
- Connection Management: Automatic reconnection to target server with connection state tracking
- Configurable Timeouts: Backend timeout protection prevents indefinite hanging
- Comprehensive Metrics: Built-in metrics tracking with JSON snapshot output
- Structured Logging: Separate log files for regular logs and metrics with daily rotation
- Strict Mode: Configurable error mapping (GatewayPathUnavailable vs ServerDeviceFailure)
- High Performance: Built on Tokio for async I/O handling
- Concurrent Connections: Supports multiple simultaneous client connections
- Rust 1.70+
- Cargo
# Clone and build
git clone <repository-url>
cd modbus-proxy
cargo build --release
# Run proxy
cargo run --release -- --target 192.168.1.100:502
# With custom options
cargo run --release -- --target 192.168.1.100:502 --listen 127.0.0.1:8080 --log-dir ./logsdocker build -t modbus-proxy .
docker run -p 5020:5020 modbus-proxy --target 192.168.1.100:502┌─────────────────┐
│ Modbus Clients │
└────────┬────────┘
│
│ Connects to
│
┌────────▼────────┐
│ Modbus TCP Proxy│
│ (Port 5020) │
└────────┬────────┘
│
│ Forwards requests with
│ Unit-ID passthrough
│
┌────────▼────────┐
│ Target Modbus │
│ Server │
│ (Port 502) │
└─────────────────┘
| Option | Default | Description |
|---|---|---|
-t, --target <ADDR> |
Required | Target Modbus server address |
-l, --listen <ADDR> |
0.0.0.0:5020 |
Listen address for proxy |
--strict <BOOL> |
true |
Use GatewayPathUnavailable for errors |
--log-dir <DIR> |
./logs |
Log directory for file output |
--backend-timeout <SECS> |
30 |
Backend timeout in seconds |
--metrics-json |
- | Output metrics as JSON and exit |
--no-console |
false |
Disable console logging |
-h, --help |
- | Print help information |
RUST_LOG: Logging level (error,warn,info,debug,trace)
# Basic usage
cargo run --release -- --target 192.168.1.100:502
# Custom listen port
cargo run --release -- --target 192.168.1.100:502 --listen 0.0.0.0:5021
# Non-strict mode (ServerDeviceFailure errors)
cargo run --release -- --target 192.168.1.100:502 --strict false
# Disable console output
cargo run --release -- --target 192.168.1.100:502 --no-console
# Output metrics JSON
cargo run --release -- --metrics-json
# Debug logging
RUST_LOG=debug cargo run --release -- --target 192.168.1.100:5020x01: Read Coils0x02: Read Discrete Inputs0x03: Read Holding Registers0x04: Read Input Registers0x05: Write Single Coil0x06: Write Single Register0x0F: Write Multiple Coils0x10: Write Multiple Registers
Unsupported function codes return IllegalFunction exception.
Metrics are logged every 30 seconds to a separate metrics file and can be viewed via JSON output.
{
"timestamp": "2025-11-11T15:30:00Z",
"connections": {
"client": {
"total": 15,
"active": 3
},
"target": {
"total": 8,
"connected": true
}
},
"requests": {
"read_coils": 45,
"read_discrete_inputs": 12,
"read_holding_registers": 156,
"read_input_registers": 89,
"write_single_coil": 8,
"write_single_register": 23,
"write_multiple_coils": 3,
"write_multiple_registers": 7
}
}Two types of log files with daily rotation:
- Regular Logs (
modbus-proxy.{target}.log): General application logs - Metrics Logs (
modbus-proxy.{target}.metrics): Metrics snapshots
Target addresses are sanitized for filenames:
192.168.1.100:502→modbus-proxy.192.168.1.100-502.log
Configurable timeout protection prevents hanging on slow/unresponsive targets.
Strict Mode (default):
- Returns
GatewayPathUnavailable(0x0A) exception on timeout - Connection is dropped and reconnects on next request
Non-Strict Mode:
- Returns
ServerDeviceFailure(0x04) exception on timeout - Connection is dropped and reconnects on next request
# Set custom timeout (1-60 seconds)
cargo run --release -- --target 192.168.1.100:502 --backend-timeout 10# Run tests
cargo test
# Run with output
cargo test -- --nocapture
# Code quality
cargo clippy
cargo fmt
# Build release
cargo build --releaseBinary will be at target/release/modbus-proxy
- Concurrent Connections: Multiple simultaneous client connections
- Async I/O: Built on Tokio for high-performance operations
- Connection Pooling: Efficient target server connection reuse
- Low Latency: Direct request forwarding with minimal overhead
-
Cannot connect to target:
- Verify target address and port
- Check network connectivity with
pingortelnet - Ensure target server is running and accessible
- Check firewall rules
-
Port already in use:
- Change listen port:
--listen 0.0.0.0:5021 - Or stop the service using the port
- Change listen port:
-
Client cannot connect:
- Verify proxy is listening:
netstat -tln | grep 5020 - Check firewall settings
- Verify proxy is listening:
# Update Rust
rustup update
# Clean and rebuild
cargo clean && cargo build- Ensure log directory exists and is writable
- Check
RUST_LOGenvironment variable - Reduce log level:
RUST_LOG=info
- Fork the repository
- Create feature branch:
git checkout -b feature/amazing-feature - Make changes
- Run tests:
cargo test - Run clippy:
cargo clippy - Format code:
cargo fmt - Commit:
git commit -m 'Add amazing feature' - Push:
git push origin feature/amazing-feature - Submit pull request
This project is licensed under the MIT License - see the LICENSE file for details.
- Built with tokio-modbus
- Uses Tokio for async runtime
- Logging with tracing
- Command-line parsing with clap
- Error handling with anyhow