Skip to content

cortexuvula/clawreachbridge

Repository files navigation

ClawReach Bridge

Secure WebSocket proxy that connects ClawReach mobile/web clients to OpenClaw Gateway over Tailscale

ClawReach Bridge is the missing link between the ClawReach multi-platform chat client and an OpenClaw Gateway instance running on your network. It acts as a trusted intermediary — accepting WebSocket connections from ClawReach apps over Tailscale's encrypted mesh and forwarding them to the Gateway with the correct Origin headers, so Gateway accepts the connection without any modifications.

Why is this needed?

OpenClaw Gateway enforces strict WebSocket origin checks (GitHub issues #9358, PR #10695, CVE-2026-25253) to prevent cross-site WebSocket hijacking. ClawReach clients connecting from mobile devices or remote machines present private/Tailscale IPs that fail these checks. Rather than weakening Gateway's security model, the bridge solves this at the network layer:

  • Tailscale-only access: Only accepts connections from Tailscale network (100.64.0.0/10) — no public internet exposure
  • Origin header injection: Rewrites headers so Gateway sees a trusted localhost connection
  • Zero Gateway changes: Works with a completely unmodified OpenClaw installation
  • End-to-end encryption: Tailscale provides the security boundary between client and bridge
  • Graceful shutdown: Two-phase drain sends WebSocket close frames before terminating
  • Web Admin UI: Built-in browser dashboard for monitoring and control

Quick Start

# 1. Install
curl -fsSL https://raw.githubusercontent.com/cortexuvula/clawreachbridge/master/scripts/install.sh | bash

# 2. Setup (auto-detects Tailscale IP, writes config, starts service)
sudo clawreachbridge setup

# 3. Verify
curl http://127.0.0.1:8081/health

# 4. Open admin dashboard
open http://127.0.0.1:8081/ui/

The setup wizard will detect your Tailscale IP, prompt for Gateway URL and ports, write the config file, and optionally start the systemd service.

For manual installation, download the binary from releases and see configs/config.example.yaml for configuration reference.

Architecture

ClawReach Client (iOS / Android / Web)
        ↓ (Tailscale, encrypted)
ClawReach Bridge (this project)
        ↓ (localhost, proper Origin headers)
OpenClaw Gateway (unmodified)

The bridge binds to your machine's Tailscale IP so only devices on your tailnet can reach it. Connections to Gateway go over localhost, where Origin headers are trusted.

Features

Connection Stability

Designed for long-lived WebSocket connections with clean lifecycle management:

  • Graceful close frames: Clients receive proper WebSocket close frames with status codes and reasons instead of raw TCP resets, letting client-side reconnection logic distinguish between intentional shutdowns and network failures.
  • Two-phase shutdown: On SIGTERM/SIGINT, the bridge stops accepting new connections, sends StatusGoingAway close frames to all active clients, waits for drain (up to drain_timeout), then force-closes any remaining.
  • Keepalive pings: Periodic WebSocket pings detect dead connections. Failed pings send a close frame with "keepalive timeout" before teardown.
  • Circuit breaker: Fast-fails new connections when the gateway is down instead of blocking on dial timeout. Configurable failure/success thresholds and recovery timeout.
  • Adaptive write timeout: Large messages (file attachments) get scaled timeouts based on min_write_bytes_per_sec instead of a fixed deadline, preventing legitimate transfers from being killed.
  • Stream drain: When a client disconnects mid-stream, the bridge continues reading from the gateway to capture the final or aborted AI response for cross-device sync.
  • Abort-aware: chat.abort (state "aborted") is recognized as terminal throughout — stored in sync history, captured by the observer, and handled by stream drain. Partial text is preserved with a stopReason field.
  • Tunable timeouts: write_timeout, ping_interval, pong_timeout, and dial_timeout are all independently configurable.
Scenario Close Code Reason
Gateway unreachable 1014 (Bad Gateway) gateway unreachable
Circuit breaker open 1014 (Bad Gateway) gateway circuit breaker open
Keepalive failure 1001 (Going Away) keepalive timeout
Server shutdown 1001 (Going Away) server shutting down

Security Hardening

  • Tailscale-only: Validates client IPs against Tailscale CGNAT ranges (IPv4 100.64.0.0/10, IPv6 fd7a:115c:a1e0::/48)
  • Auth tokens: Header-based (Authorization: Bearer) primary, query param (?token=) fallback
  • Admin token: Separate token for web admin API mutation endpoints
  • Per-IP rate limiting: Token bucket rate limiter for connection attempts and message throughput
  • Connection limits: Global and per-IP caps with atomic check-and-increment (no TOCTOU race)
  • Public paths: Configurable path prefixes that bypass auth (e.g., A2UI static assets served to WebViews)
  • HTTP body limits: Non-WebSocket requests are capped at max_http_body_size (default 10MB) to prevent memory exhaustion
  • File receive limits: Per-file and cumulative session budgets for received attachments, with automatic cleanup of old inbox files (inbox_max_age)
  • Bounded inspector buffering: Messages exceeding max_inspected_message_size bypass inspectors and stream directly, with message-type classification (file/chat/other) logged at Warn and tracked via metrics

Per-Connection Trace IDs

Every connection gets a unique 8-character hex trace ID (X-Trace-Id header), propagated through all log lines for that connection. Makes correlating logs across proxy, inspector, and cleanup phases trivial.

Doctor Command

clawreachbridge doctor runs pre-flight diagnostics:

  • Config file validation
  • Gateway reachability check (HTTP + WebSocket)
  • Tailscale status verification
  • systemd service status
  • Port conflict detection

Supports --json output for CI/monitoring integration.

Cross-Device Message Sync

Captures chat messages flowing through the bridge and enables cross-device history:

  • In-memory ring buffer with configurable depth per session (max_history, default 200)
  • JSONL disk persistence: Survives bridge restarts. Automatic compaction when append count exceeds 2x buffer size.
  • Session eviction: TTL-based expiry (session_ttl, default 24h) and max sessions cap (max_sessions, default 100) prevent unbounded memory growth. Background cleanup goroutine runs automatically.
  • User echo broadcast: When a user sends a message from one device, sibling clients on the same session receive a synthetic chat event echo immediately.
  • History interception: sessions.history requests are answered from the bridge's store without hitting the gateway.
  • Attachment support: File attachments (images, audio, documents) are stored alongside text content.
  • Aborted message capture: AI responses stopped via chat.abort are stored with partial text and stopReason metadata, so clients can render "(stopped)" indicators.
bridge:
  sync:
    enabled: true
    max_history: 200
    data_dir: "/var/lib/clawreachbridge/sync"
    max_sessions: 100       # evict oldest when exceeded
    session_ttl: "24h"      # evict sessions idle longer than this

Media Injection

When OpenClaw generates images, they are saved to the gateway's outbound media directory but not included in WebSocket chat messages. The bridge detects these images and injects them inline:

  1. Tracks chat run IDs from streaming delta messages
  2. On final chat message, scans the media directory for images created during that run
  3. Deduplicates across scans to prevent re-injection
  4. Base64-encodes matching images and appends as { type: "image" } content items

Setup: The setup wizard prompts for the media directory path. When specified, it automatically adds the bridge.media section and creates a systemd override for file access.

Manual configuration:

bridge:
  media:
    enabled: true
    directory: "/home/youruser/.openclaw/media/outbound"
    max_file_size: 10485760   # 10MB max per image
    max_age: "60s"            # Only inject images created within this window
    extensions: [".png", ".jpg", ".jpeg", ".webp", ".gif"]
    inject_paths: []          # Empty = all paths; set prefixes to restrict
    allowed_dirs: []          # Restrict MEDIA: path resolution (default-deny when set)
    max_receive_file_size: 10485760   # 10MB per received file attachment
    inbox_max_total_size: 104857600   # 100MB cumulative session budget
    inbox_max_age: "24h"              # Auto-cleanup received files older than this

Canvas State Tracking

Shadows canvas.present, canvas.hide, and canvas.a2ui.pushJSONL messages from the gateway and replays them to reconnecting clients, so WebView-based surfaces survive connection drops.

bridge:
  canvas:
    state_tracking: true
    jsonl_buffer_size: 5    # Recent JSONL payloads to retain
    max_age: "5m"           # Discard stale canvas state
    a2ui_url: ""            # Optional: inject A2UI URL into canvas.present

Persistent Observer Connection

Maintains a long-lived WebSocket connection to the gateway to capture messages when no client is connected (e.g., overnight agent runs). Messages are stored via the sync layer and available to clients on reconnect.

  • Automatic reconnect: Reconnects with exponential backoff on disconnection
  • File attachment download: Captures file attachments from messages and stores them locally with retry logic and budget enforcement
  • Active client suppression: Pauses observer capture when real clients are connected to avoid duplicates
  • Configurable session key: Targets a specific gateway session (default: agent:main:main)
observer:
  enabled: true
  session_key: "agent:main:main"
  file_storage:
    enabled: true
    directory: "/var/lib/clawreachbridge/observer-files"
    max_file_size: 52428800      # 50MB per file
    max_total_size: 5368709120   # 5GB total
    max_age: "168h"              # 7 days

File Delivery Reliability

The proxy pipeline includes specific protections against silent message loss:

  • Write failure tracking: All 10 write-failure paths in the forwarding loop log at Warn with structured metadata (write_path, payload_size, connection_uptime) and increment the write_failures_total metric. No message is silently dropped.
  • Inspector bypass visibility: When a message exceeds max_inspected_message_size and bypasses inspectors, the first 4KB is classified as file/chat/other and tracked via the inspection_bypassed_total metric.
  • Atomic budget reservation: Observer file storage uses optimistic reservation to prevent concurrent downloads from exceeding the total size budget (TOCTOU race fix).
  • Inbox cleanup: Received file attachments are automatically cleaned up after inbox_max_age, with budget recalculation.

Prometheus Metrics

Optional metrics served on the health listener (/metrics):

  • clawreachbridge_connections_total — total WebSocket connections accepted
  • clawreachbridge_active_connections — current open connections
  • clawreachbridge_messages_total{direction} — messages proxied (upstream/downstream)
  • clawreachbridge_errors_total{type} — errors by type (accept_failure, dial_failure, tailscale_rejected, auth_rejected, rate_limit_exceeded, connection_limit, circuit_breaker_open, subprotocol_rejected)
  • clawreachbridge_reactions_total{emoji} — reaction events
  • clawreachbridge_canvas_events_total{action} — canvas state events
  • clawreachbridge_canvas_replays_total — canvas replays on reconnect
  • clawreachbridge_circuit_breaker_state — circuit breaker state (0=closed, 1=open, 2=half-open)
  • clawreachbridge_sync_messages_total{role} — sync messages stored
  • clawreachbridge_sync_sessions_active — active sync sessions
  • clawreachbridge_sync_broadcasts_total — sync broadcast operations
  • clawreachbridge_sync_jsonl_* — JSONL persistence metrics (errors, compactions, corrupt lines)
  • clawreachbridge_messages_inspection_bypassed_total{direction,message_hint} — messages that bypassed inspectors due to size
  • clawreachbridge_write_failures_total{direction,write_path} — message write failures by path (bypass/inspected/streaming)
  • clawreachbridge_fcm_push_total{status} — FCM push notification operations
  • clawreachbridge_fcm_tokens_active — registered FCM device tokens
monitoring:
  metrics_enabled: true
  metrics_endpoint: "/metrics"

Web Admin UI

Built-in web dashboard — no extra install, no separate process. Starts automatically whenever the bridge is running.

To use it: open http://127.0.0.1:8081/ui/ in any browser on the machine running the bridge.

The UI is embedded in the single binary via go:embed and served on the health listener. It provides five tabs:

  • Dashboard: Real-time status — uptime, active/total connections, messages proxied, gateway reachability, memory usage, goroutine count, build info. Auto-refreshes every 3 seconds.
  • Connections: Per-IP connection breakdown sorted by count. Auto-refreshes every 5 seconds.
  • Config: View and edit reloadable settings (log level, connection limits, rate limits, message size). Changes are in-memory only. Read-only settings shown separately.
  • Logs: Live log viewer with level filtering and incremental auto-refresh. Up to 1000 recent entries from a ring buffer.
  • Controls: Reload config from disk (equivalent to kill -HUP) or restart service via systemd.

Security: The admin UI is served on the health listener (127.0.0.1:8081) which is localhost-only. Mutation endpoints require Content-Type: application/json. Auth token values are never exposed via the config API. An optional admin_token protects mutation endpoints.

API

The web UI is powered by a JSON API available at /api/v1/ on the health listener:

Method Path Description
GET /api/v1/status Dashboard data (uptime, connections, memory, version)
GET /api/v1/connections Per-IP active connection breakdown
GET /api/v1/config Current config (reloadable + read-only, auth token masked)
PUT /api/v1/config Update reloadable config fields (in-memory only)
GET /api/v1/logs?limit=100&level=info&since=<RFC3339> Recent log entries from ring buffer
POST /api/v1/reload Reload config from disk
POST /api/v1/restart Restart service via systemd

Configuration

Key settings in /etc/clawreachbridge/config.yaml (see configs/config.example.yaml for all options):

Setting Default Description
bridge.listen_address 100.64.0.1:8080 Tailscale IP + port to bind
bridge.gateway_url http://localhost:18800 OpenClaw Gateway upstream
bridge.drain_timeout 30s Max wait for connections to close on shutdown
bridge.write_timeout 30s Base deadline for writing a single message
bridge.min_write_bytes_per_sec 0 Floor for adaptive write timeout (0 = fixed timeout)
bridge.ping_interval 30s WebSocket ping frequency for dead peer detection
bridge.pong_timeout 10s Max wait for pong response
bridge.max_message_size 262144 Max WebSocket message size (256KB)
bridge.max_http_body_size 10485760 Max HTTP body for non-WS requests (10MB)
bridge.media.enabled false Enable image injection from media directory
bridge.sync.enabled false Enable cross-device message sync
bridge.sync.max_sessions 100 Max sessions to retain (0 = unlimited)
bridge.sync.session_ttl 24h Evict sessions idle longer than this
bridge.canvas.state_tracking false Shadow canvas state for reconnect replay
bridge.circuit_breaker.enabled false Fast-fail when gateway is down
observer.enabled false Enable persistent observer connection
security.tailscale_only true Require Tailscale IP
security.auth_token "" Optional auth token for proxy connections
security.admin_token "" Optional auth token for admin API mutations
security.max_connections 1000 Global connection limit
security.max_connections_per_ip 10 Per-IP connection limit
monitoring.metrics_enabled false Enable Prometheus metrics

All settings support environment variable overrides with the CLAWREACH_ prefix (e.g. CLAWREACH_BRIDGE_WRITE_TIMEOUT=60s).

Documentation

Requirements

  • OpenClaw: Gateway must be running on the same machine (typically on localhost:18800)
  • Tailscale: Must be installed and running on both the bridge machine and client devices
  • ClawReach: The mobile/web client that connects through the bridge
  • OS: Linux (primary), macOS (supported)

License

MIT (or Apache 2.0 - TBD)

Contributing

See IMPLEMENTATION_PLAN.md § 16 for guidelines.

Credits

Created by Andre Hugo (@cortexuvula) to connect ClawReach clients to OpenClaw Gateway without modifying either.

Built with Claude Code Opus 4.6 from Fred's implementation plan.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •  

Languages