Sign inSign up

obeoneorg/simplex-ws-proxy

By obeoneorg

Updated 22 days ago

Image
0

2.7K

obeoneorg/simplex-ws-proxy repository overview

Python WebSockets Docker License

simplex-ws-proxy

WebSocket proxy for SimpleX Chat CLI — multiplex one upstream connection to N downstream clients with transparent corrId routing.


📋 Table of Contents

Architecture · Features · How it works · Installation · Configuration · Usage · Development · License


🏗️ Architecture

flowchart TB
    subgraph Downstream["Downstream clients"]
        C1[Client 1]
        C2[Client 2]
        C3[Client N]
    end

    subgraph Proxy["simplex-ws-proxy :6225"]
        direction TB
        REG[ClientRegistry]
        RT[RoutingTable]
        Q1[Queue 1]
        Q2[Queue 2]
        Q3[Queue N]
    end

    C1 -->|cmd + corrId| Proxy
    C2 -->|cmd + corrId| Proxy
    C3 -->|cmd + corrId| Proxy

    Proxy -->|rewritten corrId| S[SimpleX CLI :5225]
    S -->|response / event| Proxy

    Proxy -->|routed response| C1
    Proxy -->|broadcast event| C2
    Proxy -->|broadcast event| C3

    style Proxy fill:#4a9eff,stroke:#2277cc,color:#fff
    style S fill:#222,stroke:#444,color:#fff

The proxy maintains exactly one upstream WebSocket connection to SimpleX CLI. Each downstream client gets its own message queue and corrId namespace — responses are routed back to the originating client, events are broadcast to everyone.


🚀 Features

FeatureDescription
1-to-N multiplexingAny number of clients share a single upstream SimpleX CLI connection
Transparent corrId routingCommands from each client are tagged with a proxy-level corrId; responses are demultiplexed and delivered only to the originating client
Event broadcastUnsolicited events (no corrId) are forwarded to all connected clients
Response broadcast opt-inClients connecting with ?broadcast_responses=true receive responses routed to any other connected client, in addition to their own
Binary passthroughBinary (bytes) messages are forwarded transparently between upstream and downstream without any transformation
Initial state replayNew clients immediately receive the last cached upstream state on connect
Upstream reconnectExponential backoff reconnection (1 → 2 → 4 → 8 → 16 → 30 s) with proxyEvent notification to all clients on disconnect
Health check endpointBuilt-in HTTP GET /health endpoint returns 200 OK — compatible with Docker HEALTHCHECK and orchestrator liveness probes
Bounded queuesPer-client queues (max 1 000 messages) drop oldest messages for slow clients — no head-of-line blocking
Stale entry cleanupRouting table entries expire after a configurable timeout to prevent memory leaks
JSON error handlingInvalid JSON from a client gets a structured error response instead of crashing the connection
Graceful shutdownSIGINT / SIGTERM handled cleanly
Structured loggingColor-coded, leveled logging via coloredlogs

🔍 How it works

SimpleX CLI speaks a simple JSON protocol over WebSocket. Each command carries a corrId field; the response echoes the same corrId back so the caller can match request to reply.

The problem: a single WebSocket connection means a single corrId namespace. Two clients sending {"corrId": "1", ...} simultaneously would collide.

simplex-ws-proxy solves this by rewriting corrId on the way out:

Client A sends:    {"corrId": "1", "cmd": "/contacts"}
Proxy forwards:    {"corrId": "proxy_<clientA-uuid>_1", "cmd": "/contacts"}

SimpleX responds:  {"corrId": "proxy_<clientA-uuid>_1", "resp": {...}}
Proxy delivers:    {"corrId": "1", "resp": {...}}  →  Client A only

Events without a corrId (e.g. incoming messages) are broadcast to all clients unchanged.

Upstream disconnect notification

When the upstream SimpleX CLI connection drops, every connected client receives a synthetic event:

{
  "corrId": "",
  "resp": {
    "type": "proxyEvent",
    "event": "upstreamDisconnected"
  }
}

The proxy then reconnects automatically with exponential backoff.


📦 Installation

docker run -d \
  -p 6225:6225 \
  -e SIMPLEX_WS_URL=ws://simplex-cli:5225 \
  obeoneorg/simplex-ws-proxy

Or with Docker Compose:

cp docker-compose.example.yml docker-compose.yml
# Edit docker-compose.yml to match your setup
docker compose up -d

The Docker image includes a built-in HEALTHCHECK that polls the /health endpoint every 30 s.

Local
git clone https://github.com/obeone/simplex-ws-proxy.git
cd simplex-ws-proxy
uv venv && source .venv/bin/activate
uv pip install -e .

⚙️ Configuration

Settings can be provided as CLI flags or environment variables. CLI flags take precedence.

CLI flagEnv variableDefaultDescription
--upstreamSIMPLEX_WS_URLws://localhost:5225WebSocket URL of the upstream SimpleX CLI
--hostPROXY_HOST0.0.0.0Host address to listen on
--portPROXY_PORT6225Port to listen on
--stale-timeoutSTALE_TIMEOUT60Seconds before a routing table entry expires
--log-levelLOG_LEVELINFOLogging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)

🔧 Usage

# With defaults (connects to ws://localhost:5225, listens on :6225)
simplex-ws-proxy

# Using CLI flags
simplex-ws-proxy --upstream ws://simplex:5225 --log-level DEBUG

# Using environment variables
SIMPLEX_WS_URL=ws://simplex:5225 LOG_LEVEL=DEBUG simplex-ws-proxy
Connect a client
import asyncio
import websockets

async def main():
    async with websockets.connect("ws://localhost:6225") as ws:
        await ws.send('{"corrId": "abc123", "cmd": "/contacts"}')
        async for message in ws:
            print(message)  # routed responses + broadcast events

asyncio.run(main())
Broadcast responses opt-in

By default each client's command responses are private — only the originating client receives them. A client can opt into receiving responses routed to any other connected client by appending ?broadcast_responses=true to its connection URL:

async with websockets.connect(
    "ws://localhost:6225?broadcast_responses=true"
) as ws:
    ...

When this flag is set, the proxy forwards to this client every response it routes to any other client, in addition to the client's own responses. Unsolicited events (messages without a corrId) are always broadcast to everyone regardless of this setting.

Typical use-case: a monitoring or logging client that wants to observe all command traffic on a shared proxy without being the one issuing the commands.

Health check

The proxy exposes an HTTP GET /health endpoint on the same port as the WebSocket server. It returns 200 OK when the proxy is running:

curl http://localhost:6225/health

A standalone healthcheck.py script is bundled in the Docker image for use as a HEALTHCHECK command. It reads the PROXY_PORT environment variable to find the correct port.


🧪 Development

CommandDescription
uv pip install -e ".[dev]"Install with dev dependencies
pytestRun tests
pytest --covRun tests with coverage
ruff check .Lint
ruff format .Format

📄 License

MIT — see LICENSE for details.

Made by obeone

Tag summary

Content type

Image

Digest

sha256:758caf656

Size

46.3 MB

Last updated

22 days ago

docker pull obeoneorg/simplex-ws-proxy