|
| 1 | +# mbproxy |
| 2 | + |
| 3 | +A lightweight Modbus TCP proxy with in-memory caching. Designed to reduce load on Modbus devices by serving cached responses to multiple clients. |
| 4 | + |
| 5 | +## Features |
| 6 | + |
| 7 | +- **Caching**: In-memory cache with configurable TTL |
| 8 | +- **Request coalescing**: Identical concurrent requests share a single upstream fetch |
| 9 | +- **Read-only mode**: Optionally block or ignore write requests |
| 10 | +- **Auto-reconnect**: Automatic upstream reconnection on failure |
| 11 | +- **Stale data fallback**: Optionally serve stale cache on upstream errors |
| 12 | +- **Graceful shutdown**: Complete in-flight requests before terminating |
| 13 | +- **Minimal footprint**: ~6MB Docker image (scratch base) |
| 14 | + |
| 15 | +## Quick Start |
| 16 | + |
| 17 | +```bash |
| 18 | +docker run --rm \ |
| 19 | + -e MODBUS_UPSTREAM=192.168.1.100:502 \ |
| 20 | + -p 5502:5502 \ |
| 21 | + ghcr.io/tma/mbproxy |
| 22 | +``` |
| 23 | + |
| 24 | +## Configuration |
| 25 | + |
| 26 | +All configuration is via environment variables: |
| 27 | + |
| 28 | +| Variable | Description | Default | |
| 29 | +|----------|-------------|---------| |
| 30 | +| `MODBUS_LISTEN` | TCP address to listen on | `:5502` | |
| 31 | +| `MODBUS_UPSTREAM` | Upstream Modbus device address | (required) | |
| 32 | +| `MODBUS_SLAVE_ID` | Default slave ID | `1` | |
| 33 | +| `MODBUS_CACHE_TTL` | Cache time-to-live | `10s` | |
| 34 | +| `MODBUS_CACHE_SERVE_STALE` | Serve stale data on upstream error | `false` | |
| 35 | +| `MODBUS_READONLY` | Read-only mode: `false`, `true`, `deny` | `true` | |
| 36 | +| `MODBUS_TIMEOUT` | Upstream connection timeout | `10s` | |
| 37 | +| `MODBUS_SHUTDOWN_TIMEOUT` | Graceful shutdown timeout | `30s` | |
| 38 | +| `LOG_LEVEL` | Log level: `INFO`, `DEBUG` | `INFO` | |
| 39 | + |
| 40 | +### Read-Only Modes |
| 41 | + |
| 42 | +- `false`: Full read/write passthrough to upstream device |
| 43 | +- `true`: Silently ignore write requests, return success response |
| 44 | +- `deny`: Reject write requests with Modbus illegal function exception |
| 45 | + |
| 46 | +## Docker Compose Examples |
| 47 | + |
| 48 | +### Basic Setup |
| 49 | + |
| 50 | +```yaml |
| 51 | +services: |
| 52 | + mbproxy: |
| 53 | + image: ghcr.io/tma/mbproxy |
| 54 | + ports: |
| 55 | + - "5502:5502" |
| 56 | + environment: |
| 57 | + MODBUS_UPSTREAM: "192.168.1.100:502" |
| 58 | + restart: unless-stopped |
| 59 | +``` |
| 60 | +
|
| 61 | +### All Configuration Options |
| 62 | +
|
| 63 | +```yaml |
| 64 | +services: |
| 65 | + mbproxy: |
| 66 | + image: ghcr.io/tma/mbproxy |
| 67 | + ports: |
| 68 | + - "5502:5502" |
| 69 | + environment: |
| 70 | + MODBUS_LISTEN: ":5502" |
| 71 | + MODBUS_UPSTREAM: "192.168.1.100:502" |
| 72 | + MODBUS_SLAVE_ID: "1" |
| 73 | + MODBUS_CACHE_TTL: "10s" |
| 74 | + MODBUS_CACHE_SERVE_STALE: "false" |
| 75 | + MODBUS_READONLY: "true" |
| 76 | + MODBUS_TIMEOUT: "10s" |
| 77 | + MODBUS_SHUTDOWN_TIMEOUT: "30s" |
| 78 | + LOG_LEVEL: "INFO" |
| 79 | + restart: unless-stopped |
| 80 | +``` |
| 81 | +
|
| 82 | +### Multiple Devices (Multiple Proxies) |
| 83 | +
|
| 84 | +```yaml |
| 85 | +services: |
| 86 | + inverter-proxy: |
| 87 | + image: ghcr.io/tma/mbproxy |
| 88 | + ports: |
| 89 | + - "5502:5502" |
| 90 | + environment: |
| 91 | + MODBUS_UPSTREAM: "192.168.1.100:502" |
| 92 | + MODBUS_CACHE_TTL: "10s" |
| 93 | + |
| 94 | + meter-proxy: |
| 95 | + image: ghcr.io/tma/mbproxy |
| 96 | + ports: |
| 97 | + - "5503:5502" |
| 98 | + environment: |
| 99 | + MODBUS_UPSTREAM: "192.168.1.101:502" |
| 100 | + MODBUS_CACHE_TTL: "2s" |
| 101 | +``` |
| 102 | +
|
| 103 | +## Building from Source |
| 104 | +
|
| 105 | +```bash |
| 106 | +# Build Docker image |
| 107 | +docker build -t mbproxy . |
| 108 | + |
| 109 | +# Run tests |
| 110 | +docker build --target test . |
| 111 | + |
| 112 | +# Or run tests directly |
| 113 | +docker run --rm -v $(pwd):/app -w /app golang:1.24 go test ./... |
| 114 | +``` |
| 115 | + |
| 116 | +## Supported Modbus Functions |
| 117 | + |
| 118 | +| Code | Function | |
| 119 | +|------|----------| |
| 120 | +| 0x01 | Read Coils | |
| 121 | +| 0x02 | Read Discrete Inputs | |
| 122 | +| 0x03 | Read Holding Registers | |
| 123 | +| 0x04 | Read Input Registers | |
| 124 | +| 0x05 | Write Single Coil | |
| 125 | +| 0x06 | Write Single Register | |
| 126 | +| 0x0F | Write Multiple Coils | |
| 127 | +| 0x10 | Write Multiple Registers | |
| 128 | + |
| 129 | +## Cache Behavior |
| 130 | + |
| 131 | +- **Key format**: `{slave_id}:{function_code}:{start_address}:{quantity}` |
| 132 | +- **Read requests**: Served from cache if available and not expired |
| 133 | +- **Write requests**: Forwarded to upstream (if allowed), exact matching cache entries invalidated |
| 134 | +- **Request coalescing**: Multiple identical requests during a cache miss share a single upstream fetch |
| 135 | + |
| 136 | +## License |
| 137 | + |
| 138 | +MIT |
0 commit comments