Skip to content

[bug] WebSocket PTY rejects connections from Docker bridge IPs #149

@ThePlenkov

Description

@ThePlenkov

Bug Description

When CAO runs inside a Docker container and the browser connects from the host, WebSocket connections to the PTY endpoint are rejected because the client IP is a Docker bridge address (e.g. 172.17.0.1) rather than 127.0.0.1.

Steps to Reproduce

  1. Run cao-server --host 0.0.0.0 inside a Docker container with port 9889 mapped
  2. Open the CAO web UI from the host browser at http://localhost:9889
  3. Try to connect to a terminal session via WebSocket

Expected Behavior

WebSocket connection succeeds — the host browser can interact with tmux terminal sessions.

Actual Behavior

WebSocket connection is rejected. The server logs show the client IP as 172.17.0.1 (Docker bridge), which fails the localhost check.

Root Cause

In cli_agent_orchestrator/api/main.py, the WebSocket endpoint has a hardcoded localhost check:

if client_host not in (None, "127.0.0.1", "::1", "localhost"):
    await websocket.close(code=1008, reason="Only localhost connections are allowed")
    return

Docker bridge IPs (172.x.x.x), which are standard for container-to-host networking, are not in this allowlist.

Proposed Fix

Make the trusted hosts configurable, with sensible defaults that include private networks:

# Option A: Environment variable
TRUSTED_HOSTS = os.environ.get("CAO_TRUSTED_HOSTS", "").split(",") if os.environ.get("CAO_TRUSTED_HOSTS") else []
DEFAULT_HOSTS = {None, "127.0.0.1", "::1", "localhost"}

if client_host not in DEFAULT_HOSTS and client_host not in TRUSTED_HOSTS:
    # Also allow RFC 1918 private ranges (Docker bridge, Kubernetes pods, etc.)
    is_private = any(client_host and client_host.startswith(prefix) 
                     for prefix in ("172.", "10.", "192.168."))
    if not is_private:
        await websocket.close(code=1008, reason="Only localhost/private connections allowed")
        return

Or, more conservatively, add a --trusted-hosts CLI flag to cao-server.

Workaround

We currently patch this at install time with:

if client_host not in (None, "127.0.0.1", "::1", "localhost") and not (client_host and client_host.startswith("172.")):

Environment

  • CAO: main branch
  • Docker: 27.x
  • OS: Linux (Docker container based on python:3.13)
  • Browser: Chrome (connecting from Docker host)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions