WebSocket / HTTP relay for OpenBitFun Remote Connect and account login.
Open-source OpenBitFun does not ship a public hosted login service. If you want Desktop / CLI account login, cross-device session & settings sync, or Peer Device Mode (control another online device on the same account), you must:
- Deploy this relay yourself
- Enable the account database (
RELAY_DB_PATH) - Create user accounts out-of-band with
relay-admin(no public sign-up) - Point OpenBitFun Desktop or CLI at your relay URL and log in
The relay stays zero-knowledge: clients encrypt with a master key derived locally; the server stores Argon2id password hashes and AES-GCM-wrapped keys, never plaintext passwords or decryptable sync payloads.
One-click Docker deploy (bash deploy.sh) targets:
| OS | CPU |
|---|---|
| Linux | amd64 (x86_64) |
| Linux | arm64 (aarch64) |
The default path requires Docker Engine plus permission to talk to its daemon.
OpenBitFun Desktop installs Docker automatically when the SSH user has root/sudo.
Docker Compose, Cargo, git, tar, and build toolchains are not required on the
customer server. Compose is used only by the explicit
deploy.sh --build-from-source maintenance path.
deploy.sh (and Desktop one-click deploy) auto-detects mainland China. Image
pulls try the verified Nanjing University GHCR accelerator, then DaoCloud, then
official GHCR, always using the same image digest. Global
mode goes directly to official GHCR. Docker Engine installation also uses a
mainland route. The Desktop wizard offers Auto / Mainland China / Global so
an operator can override inaccurate cloud-IP geolocation:
OPENBITFUN_MIRROR=cn bash deploy.sh # force China mirrors
OPENBITFUN_MIRROR=global bash deploy.sh # restore OpenBitFun-managed upstream sources
bash deploy.sh --cn-mirror
bash deploy.sh --global-mirrorEngine installation defaults to Aliyun docker-ce and mirrored get.docker.com.
The daemon's Docker Hub mirrors remain useful for explicit source builds, but
they do not accelerate GHCR; release-download.sh therefore uses GHCR-specific
repository prefixes. Switching to global restores only OpenBitFun-managed host
mirror entries. See mirror.sh for the installer/source-build knobs.
| Mode | When | What you get |
|---|---|---|
| Pure relay | RELAY_DB_PATH unset |
Room pairing + mobile HTTP ↔ Desktop WebSocket bridge only. No account login, sync, or Peer Device Mode. |
| Account-enabled | RELAY_DB_PATH set to a persistent SQLite path |
Everything above plus login, device presence, device RPC (Peer HostInvoke), encrypted session/settings sync. |
Docker Compose in this directory already enables account mode
(RELAY_DB_PATH=/app/data/openbitfun_relay.db). Manual / cargo runs must set the
variable yourself or accounts stay disabled.
- Desktop and CLI connect via WebSocket; mobile uses HTTP
- End-to-end encrypted passthrough (the server does not decrypt payloads)
- Correlation-based HTTP-to-WebSocket request-response matching
- Per-room mobile-web static file upload and serving
- Heartbeat-based connection management with configurable room TTL
- Optional zero-knowledge account storage + device routing + sync
- Docker deployment support with optional Caddy reverse proxy
Use this checklist on a machine you control (VPS, LAN server, or localhost).
OpenBitFun Desktop can SSH to your host without a manual clone. One click installs Docker when necessary, verifies the signed release image descriptor locally, pulls the latest amd64/arm64 image through the selected network route, and starts it by immutable digest. It never builds on the customer server and never silently falls back to source compilation. Pull completes before an existing Relay is stopped; startup or health failure restores the previous container. Entry points: Account Login → “一键部署到自己的服务器”, or Remote Connect → Network Relay → Self-Hosted → the same action.
- Orchestration:
src/crates/services/services-integrations/src/remote_ssh/relay_deploy.rs - Wizard + invariants:
src/web-ui/src/features/relay-deploy/README.md
Task state lives under ~/.openbitfun/relay-deploy; no repository checkout is
created. Closing the wizard cancels the remote task and restores a staged
previous container. Account passwords are provisioned locally and imported via
relay-admin import-user.
Every release publishes relay-image.json plus relay-image.json.sig
(minisign, in the same base64-wrapped format as the Desktop updater). The
descriptor fixes the canonical GHCR repository, release tag, amd64/arm64
platform set, and multi-platform manifest digest. Desktop verifies it on the
user's machine and sends the digest to the server; Docker then verifies every
manifest and layer while pulling, even through a third-party accelerator.
GitHub/GHCR stays first when a 10-second GitHub byte probe reaches 512 KiB/s;
below that floor, automatic mode tries the NJU and DaoCloud GHCR accelerators
first while retaining official GHCR as the final fallback.
The raw Relay archives still carry .sha256 and .sig files for direct binary
use and for constructing the release image in CI.
Verifying an archive by hand:
BASE=https://github.com/GCWing/OpenBitFun/releases/latest/download
ASSET=openbitfun-relay-server-x86_64-unknown-linux-gnu.tar.gz
curl -fsSLO "$BASE/$ASSET" -O "$BASE/$ASSET.sig" -O "$BASE/minisign.pub"
base64 -d <"$ASSET.sig" >"$ASSET.minisig"
minisign -Vm "$ASSET" -p minisign.pub -x "$ASSET.minisig"minisign.pub is published with every release and is the same key the Desktop
updater trusts, so it can also be pinned out-of-band once and reused.
Note this is not OS-level code signing: macOS Gatekeeper and Windows SmartScreen need Apple/Authenticode certificates, which the project does not currently hold.
git clone https://github.com/GCWing/OpenBitFun
cd OpenBitFun/src/apps/relay-server
bash deploy.shdeploy.sh must run on the target server (it does not SSH elsewhere).
Its default path requires Docker on linux/amd64 or linux/arm64 and pulls
ghcr.io/gcwing/openbitfun-relay-server:latest; it does not compile locally.
Use --build-from-source only when deliberately exercising the source path.
Clone on the server, as above, rather than uploading a Windows checkout. Git for Windows rewrites these scripts to CRLF by default, and bash then fails on the first blank line:
deploy.sh: line 37: $'\r': command not found
If that happens, strip the CR and re-run:
sed -i 's/\r$//' *.sh && bash deploy.shAfter a successful start, the script runs relay-admin list-users. If the
database has no accounts, it prints the exact add-user command to run
next (account login will not work until you create at least one user).
Verify:
curl -fsS http://127.0.0.1:9700/health
docker ps --filter name=openbitfun-relayThe published image deploy and the Compose source path both set:
RELAY_DB_PATH=/app/data/openbitfun_relay.dbData lives in the relay-server_relay-db Docker volume. If you run the binary
without Docker, export a persistent path first:
export RELAY_DB_PATH=/var/lib/openbitfun/openbitfun_relay.db
mkdir -p "$(dirname "$RELAY_DB_PATH")"
RELAY_PORT=9700 ./target/release/openbitfun-relay-serverIf the process logs RELAY_DB_PATH not set — account features disabled, login
will fail with “account features disabled” until you fix the env and restart.
There is no public registration API. Operators create users with
relay-admin (bundled in the Docker image). --db must be the same path
as RELAY_DB_PATH.
# Interactive password prompt (recommended)
docker exec -it openbitfun-relay \
/app/relay-admin --db /app/data/openbitfun_relay.db add-user --username alice
# Non-interactive (scripts / CI)
docker exec openbitfun-relay \
/app/relay-admin --db /app/data/openbitfun_relay.db add-user \
--username alice --password 'choose-a-strong-password'
# List accounts
docker exec openbitfun-relay \
/app/relay-admin --db /app/data/openbitfun_relay.db list-usersOther commands:
# Reset password (also rotates the master key — old synced blobs become unreadable)
docker exec -it openbitfun-relay \
/app/relay-admin --db /app/data/openbitfun_relay.db reset-password --username alice
# Rename (credentials / user_id unchanged)
docker exec openbitfun-relay \
/app/relay-admin --db /app/data/openbitfun_relay.db rename-user \
--username alice --new-username alice2
# Delete account and all of its relay-side data
docker exec openbitfun-relay \
/app/relay-admin --db /app/data/openbitfun_relay.db delete-user --username aliceWithout Docker, build and run the same tool from this crate:
cargo build --release -p openbitfun-relay-server
./target/release/relay-admin --db "$RELAY_DB_PATH" add-user --username aliceRelay URL examples:
- Direct:
http://<YOUR_SERVER_IP>:9700 - Localhost:
http://127.0.0.1:9700 - Behind a reverse proxy:
https://relay.example.com/relay
The client appends paths (/ws, /api/*, /r/*) to the URL you enter. Use
the /relay suffix to match the official server format
(https://remote.openbitfun.com/relay). See Reverse Proxy for nginx config.
Desktop
- Open account / login UI (or Remote Connect self-hosted settings, depending on your build).
- Set Auth Server / Relay URL to the URL above.
- Sign in with the username and password you created with
relay-admin.
CLI
- Run
openbitfun, open/login. - Fill Auth Server, Username, Password, then Login.
- After login, the CLI can act as a Peer Host for same-account Desktops.
Clients remember a non-secret hint (~/.openbitfun/account_hint.json: username +
relay URL) and an encrypted session file for restart without retyping the
password.
- Encrypted settings / session sync across devices on the same account
- Device list and online presence for that account
- Peer Device Mode: one Desktop controls another online Desktop or CLI
host over device RPC (
HostInvoke/DeviceEvent) - Same machine Desktop + CLI share one
device_id; the last successfulAuthConnectwins as the live Peer Host for that id
The supported Docker build context is now the repository root because the app uses the shared relay service:
docker build -f src/apps/relay-server/Dockerfile .
docker compose -f src/apps/relay-server/docker-compose.yml buildCopying only src/apps/relay-server is no longer sufficient; deployments must
also include src/crates/services/relay-service. The repository keeps one
Docker build layout rather than duplicating the shared service.
The standalone library facade is openbitfun_relay_server; reusable relay
runtime ownership remains in the internal openbitfun-relay-service crate.
Source builds are tagged as openbitfun-relay:<git-commit> by deploy.sh and
carry the same commit in the org.opencontainers.image.revision label. The
resolved commit is persisted in the local root-only .env, so start.sh and
restart.sh keep selecting the deployed image instead of a floating tag.
docker inspect --format '{{.Config.Image}} {{index .Config.Labels "org.opencontainers.image.revision"}}' \
openbitfun-relayThe image must report /app/openbitfun-relay-server as its command and account
mode must use /app/data/openbitfun_relay.db. A pre-1.0 deployment that still
has bitfun_relay.db must be stopped and copied with SQLite's .backup command
to the new filename before the OpenBitFun image is started. The runtime has no
fallback to the retired filename.
git clone https://github.com/GCWing/OpenBitFun
cd OpenBitFun/src/apps/relay-server
bash deploy.shRun these on the target server inside this directory:
bash start.sh
bash stop.sh
bash restart.sh
docker compose ps
docker compose logs -f relay-serverNotes:
start.shis idempotent and exits if the service is already running.stop.shexits cleanly when the service is already stopped.restart.shrestarts the service when running, or starts it when stopped.- The container uses
restart: unless-stopped.
By default the relay listens on 0.0.0.0:9700 and Compose publishes that port
on the host.
Restrict to localhost:
export RELAY_HOST_BIND_IP=127.0.0.1
bash deploy.sh# From repository root
cargo build --release -p openbitfun-relay-server
# Account-enabled (persistent DB path required for login)
export RELAY_DB_PATH="$HOME/.openbitfun-relay/openbitfun_relay.db"
mkdir -p "$(dirname "$RELAY_DB_PATH")"
RELAY_PORT=9700 ./target/release/openbitfun-relay-server- Open ports:
9700(direct), and80/443if using a reverse proxy. - Hit
http://<server-ip>:9700/health(orhttps://relay.example.com/relay/healthbehind a proxy). - Confirm
RELAY_DB_PATHif you need accounts (Compose does this for you). - Create at least one user with
relay-admin. - Fill the same relay URL into Desktop / CLI and log in.
- If you terminate TLS on a reverse proxy, raise body size and read timeouts (see sync + device RPC notes below).
- Use the
/relaysuffix in the relay URL (e.g.https://relay.example.com/relay) to match the official server format. See Reverse Proxy for nginx config.
When deploying behind a reverse proxy (Caddy, nginx, etc.), configure:
- Body size limit: at least 100 MB (sync POSTs carry large encrypted bundles)
- Read/response timeout: at least 130s (device RPC waits up to 120s)
- WebSocket upgrade: the /ws endpoint requires Connection upgrade headers
- Path prefix: serve the relay at
/relay/*(strip prefix before proxying to port 9700); serve static homepage files at/via exact-match locations
server {
listen 80;
server_name relay.example.com;
# Homepage static files (exact match)
location = / {
root /path/to/relay-server/static/homepage;
try_files /index.html =404;
}
location = /i18n.json {
root /path/to/relay-server/static/homepage;
}
location = /i18n.shared.json {
root /path/to/relay-server/static/homepage;
}
# With /relay prefix: strip prefix, proxy to relay server
# For clients configured with https://relay.example.com/relay
location = /relay {
return 301 /relay/;
}
location /relay/ {
proxy_pass http://127.0.0.1:9700/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_read_timeout 130s;
proxy_send_timeout 130s;
client_max_body_size 100m;
}
}See Caddyfile for the Caddy equivalent.
| Variable | Default | Description |
|---|---|---|
RELAY_PORT |
9700 |
Server listen port |
RELAY_STATIC_DIR |
(none) | Path to mobile web static files fallback SPA. When unset, no fallback static files are served. Docker Compose sets this to /app/static. |
RELAY_ROOM_WEB_DIR |
/tmp/openbitfun-room-web |
Directory for per-room uploaded mobile-web files. Docker Compose uses a named volume mounted at /app/room-web. |
RELAY_ASSET_STORE_MAX_BYTES |
1073741824 |
Global content-addressed asset capacity (1 GiB by default). New uploads return HTTP 507 after the limit is reached; existing content remains readable. |
RELAY_ROOM_TTL |
300 |
Idle room TTL in seconds (0 = no expiry). Active heartbeats and commands refresh activity. |
RELAY_DB_PATH |
(none) | SQLite path for account storage. Unset = pure relay (no login). Set a persistent path (Compose: /app/data/openbitfun_relay.db) to enable login, device routing, and sync. Accounts are provisioned only via relay-admin. |
RELAY_CORS_ALLOW_ORIGINS |
(none) | Comma-separated browser origin allowlist, for example https://remote.example.com. Empty means same-origin only. * is rejected when account APIs are enabled. |
RELAY_PAGE_PUBLIC_BASE_URL |
(none) | Browser-visible base URL for untrusted published Page content, for example https://pages.example.com. Configure together with RELAY_PAGE_AUTH_BASE_URL. |
RELAY_PAGE_AUTH_BASE_URL |
(none) | Browser-visible base URL for the trusted Relay Page login UI, for example https://relay.example.com/relay. It must use a different browser origin from RELAY_PAGE_PUBLIC_BASE_URL. |
Production deployments that use non-public Pages should configure both Page
base URLs. The reverse proxy must route both hosts to this Relay and preserve
the original Host; it may strip the configured path prefix before proxying.
Relay then serves Page content only on the public origin and the account login
UI only on the authentication origin. Login completes through a 60-second,
single-use callback code; the callback writes an HttpOnly, Page-path-scoped
cookie on the public origin. If the variables are omitted, same-origin login is
kept only for local/backward-compatible deployments and the server logs a
warning.
When RELAY_DB_PATH is set, database open or migration failure is fatal: the
process exits instead of silently starting without account protection. The
/health response reports account capability, room/device connection counts,
pending bridge requests, and asset-store used/capacity bytes for operational
checks and capacity alerts.
| Endpoint | Method | Description |
|---|---|---|
/health |
GET | Health check (status, version, uptime, room and connection counts) |
/api/info |
GET | Server info (name, version, protocol version) |
Zero-knowledge authentication. Clients derive an Argon2id KEK locally and send only password hashes. Brute-force protection: per-account lockout + per-IP rate limit. No public registration endpoint.
| Endpoint | Method | Description |
|---|---|---|
/api/auth/login/challenge |
POST | Fetch KDF params + wrapped master key for local derivation |
/api/auth/login |
POST | Verify password hash and issue a token; returns { token, user_id } |
/api/auth/logout |
POST | Revoke the caller's token |
/api/auth/delegate |
POST | Issue a delegated token for a paired client (authenticated caller) |
Public Pages need no session. relay Pages accept any valid account on this
Relay; private Pages accept only the owner account. The browser derives the
same Argon2id password hash as native clients, so plaintext passwords are never
sent to Relay.
| Endpoint | Method | Description |
|---|---|---|
/api/page-auth/sign-in?state=… |
GET | Trusted Relay-hosted username/password form |
/api/page-auth/login |
POST | Verify the derived password hash and issue a single-use callback code |
/api/page-auth/callback?code=… |
GET | Consume the code on the Page origin and set the scoped browser session cookie |
/api/page-auth/client.js |
GET | Browser Argon2id login client |
Used by Desktop / CLI / mobile-web for presence and Peer Device Mode RPC.
| Endpoint | Method | Description |
|---|---|---|
/api/devices |
GET | List devices for the account (online + offline) |
/api/devices/:target_device_id/rpc |
POST | Route an opaque encrypted RPC to an online device (waits up to 120s) |
/api/devices/:target_device_id |
DELETE | Remove a device registration (and drop any live WS session) |
POST /api/devices/:target_device_id/rpc waits up to 120 seconds for the
target device (RPC_TIMEOUT in
../../crates/services/relay-service/src/routes/devices.rs). Peer Device Mode
uses this for product invoke calls.
Reverse proxies in front of the relay must use a read / response timeout
≥ 120s (recommend 130s), or clients see HTTP 504 before Axum finishes.
See Caddyfile for transport http timeout settings.
| Endpoint | Method | Description |
|---|---|---|
/api/rooms/:room_id/pair |
POST | Mobile initiates pairing; relay forwards to desktop via WebSocket and waits for a response |
/api/rooms/:room_id/command |
POST | Mobile sends an encrypted command; relay forwards it to desktop and returns the response |
| Endpoint | Method | Description |
|---|---|---|
/api/rooms/:room_id/upload-web |
POST | Full upload of base64-encoded files keyed by path (10 MB body limit) |
/api/rooms/:room_id/check-web-files |
POST | Incremental check for already uploaded files by hash |
/api/rooms/:room_id/upload-web-files |
POST | Incremental upload of only missing files (10 MB body limit) |
/r/:room_id/*path |
GET | Serve uploaded mobile-web static files for a room |
| Endpoint | Method | Description |
|---|---|---|
/ws |
WebSocket | Desktop and CLI account / room clients |
Encrypted session and settings blobs. All payloads are AES-256-GCM encrypted client-side with the account master key; the relay cannot read them.
| Endpoint | Method | Description |
|---|---|---|
/api/sync/sessions |
POST | Upload/replace an encrypted session blob (64 MiB Axum body limit) |
/api/sync/sessions |
GET | List encrypted session blobs (?since=<version>) |
/api/sync/sessions/:session_id |
GET | Fetch one encrypted session blob by id |
/api/sync/sessions/:session_id |
DELETE | Soft-delete a session blob (tombstone) |
/api/sync/settings |
POST | Upload/replace the encrypted settings blob (64 MiB Axum body limit) |
/api/sync/settings |
GET | Fetch the encrypted settings blob |
Session sync posts a full encrypted session bundle. Large conversations can exceed Axum’s default ~2 MiB limit and fail with HTTP 413.
This server raises the limit on sync POSTs to 64 MiB (SYNC_BODY_LIMIT in
../../crates/services/relay-service/src/routes/sync.rs). Proxies must raise
their body limit too, or they reject uploads before Axum sees them:
# nginx — must be >= Axum SYNC_BODY_LIMIT (64M)
client_max_body_size 100M;# Caddy: request_body { max_size 100MB }When diagnosing 413s, check both the proxy and Axum. Direct host-port access only hits the Axum limit.
Desktop and CLI use WebSocket for rooms and/or account device routing. Mobile clients use the HTTP endpoints above.
// Create a room (Remote Connect room bridge)
{ "type": "create_room", "room_id": "optional-id", "device_id": "...", "device_type": "desktop", "public_key": "base64..." }
// Respond to a bridged HTTP request (pair or command)
{ "type": "relay_response", "correlation_id": "...", "encrypted_data": "base64...", "nonce": "base64..." }
// Heartbeat
{ "type": "heartbeat" }
// Account-authenticated device routing (requires RELAY_DB_PATH)
{ "type": "auth_connect", "token": "...", "device_name": "..." }
{ "type": "device_message", "target_device_id": "...", "correlation_id": "...", "encrypted_data": "base64...", "nonce": "base64..." }A second auth_connect with the same (user_id, device_id) replaces the
previous live connection (last connect wins).
{ "type": "room_created", "room_id": "..." }
{ "type": "pair_request", "correlation_id": "...", "public_key": "base64...", "device_id": "...", "device_name": "..." }
{ "type": "command", "correlation_id": "...", "encrypted_data": "base64...", "nonce": "base64..." }
{ "type": "heartbeat_ack" }
{ "type": "auth_ok", "user_id": "...", "device_id": "..." }
{ "type": "auth_error", "message": "..." }
{ "type": "incoming_device_message", "source_device_id": "...", "correlation_id": "...", "encrypted_data": "base64...", "nonce": "base64..." }
{ "type": "device_presence", "devices": [{ "device_id": "...", "device_name": "..." }] }
{ "type": "error", "message": "..." }Mobile ──HTTP──► Relay ◄──WebSocket── Desktop / CLI
│
opaque E2E payloads
(optional SQLite for
accounts / sync / devices)
- Room bridge: Desktop creates a room; mobile posts
/pairand/command; the relay correlates HTTP ↔ WebSocket without reading ciphertext. - Account plane (when
RELAY_DB_PATHis set): clients log in over HTTP, thenauth_connecton WebSocket; device RPC and sync store opaque blobs. - Per-room mobile-web files can be served at
/r/:room_id/.
relay-server/
├── src/
│ ├── main.rs # Relay server binary entry point
│ ├── config.rs # Environment-based configuration
│ └── bin/
│ └── relay_admin.rs # relay-admin CLI binary
├── static/ # Mobile-web static files
├── Cargo.toml
├── Dockerfile
├── docker-compose.yml # Sets RELAY_DB_PATH for account mode
├── Caddyfile # Optional reverse proxy (body + RPC timeouts)
├── deploy.sh
├── start.sh / stop.sh / restart.sh
├── common.sh # Shared helpers for the scripts above
└── README.md
Reusable relay state, storage, asset stores, and HTTP/WebSocket routes live in
src/crates/services/relay-service. This directory owns only the standalone
process configuration, static-file fallback, and operator CLI.
- Self-hosted Remote Connect and open-source account login use this
relay-serverdirectory. src/apps/serveris a different application and is not the relay used by Desktop / CLI / mobile Remote Connect.