{"id":51988627,"url":"https://github.com/jjairola/rustdesk-api","last_synced_at":"2026-07-30T21:30:26.052Z","repository":{"id":372583745,"uuid":"1308379250","full_name":"jjairola/rustdesk-api","owner":"jjairola","description":"Zero-config shared address book for self-hosted RustDesk","archived":false,"fork":false,"pushed_at":"2026-07-22T05:02:34.000Z","size":62,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-22T07:07:47.503Z","etag":null,"topics":["address-book","axum","hbbs","remote-desktop","rust","rustdesk","self-hosted","sqlite"],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jjairola.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-07-22T04:32:28.000Z","updated_at":"2026-07-22T05:02:41.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/jjairola/rustdesk-api","commit_stats":null,"previous_names":["jjairola/rustdesk-api"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/jjairola/rustdesk-api","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jjairola%2Frustdesk-api","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jjairola%2Frustdesk-api/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jjairola%2Frustdesk-api/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jjairola%2Frustdesk-api/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jjairola","download_url":"https://codeload.github.com/jjairola/rustdesk-api/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jjairola%2Frustdesk-api/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":36093192,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"online","status_checked_at":"2026-07-30T02:00:05.956Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["address-book","axum","hbbs","remote-desktop","rust","rustdesk","self-hosted","sqlite"],"created_at":"2026-07-30T21:30:23.393Z","updated_at":"2026-07-30T21:30:26.034Z","avatar_url":"https://github.com/jjairola.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# rustdesk-api\n\nA minimal self-hosted API server for [RustDesk](https://rustdesk.com).\n\nUsers log in from the RustDesk client and **every workstation registered with\nthis server appears in their address book automatically**. No manual entry, no\nsharing IDs around, no per-user setup.\n\nThat is the whole point of it. It is not a reimplementation of RustDesk Server\nPro — there are no groups, permissions, session recordings, audit logs, or web\nconsole.\n\n## How it works\n\nRustDesk clients talk to two separate things: the **ID/relay server** (`hbbs` /\n`hbbr`, which handles the actual connections) and an **API server** (this).\nThis one only does accounts and address books; you still need `hbbs`/`hbbr`\nrunning for connections to work.\n\nTwo mechanisms combine:\n\n1. **Workstations register themselves.** A client configured with an API server\n   posts its ID, hostname, OS and username to `/api/sysinfo`, then heartbeats\n   every ~15 seconds. Each machine becomes a row in the `devices` table. This\n   happens with no user logged in — it is how the address book fills itself.\n\n2. **Everyone reads the same shared book.** Logging in gets you two address\n   books:\n\n   | Book | Contents | Writable |\n   |---|---|---|\n   | **All Workstations** | every registered device, generated live | no (`rule: 1`) |\n   | **My address book** | whatever you put in it | yes |\n\n   \"All Workstations\" is identical for every user and cannot be edited — it's a\n   view over the devices table, so it can't drift from reality. Personal books\n   are private per user; aliases, tags and saved passwords persist there.\n\nThe client's **Accessible devices** tab shows the same set, as a single device\ngroup also called \"All Workstations\". The user list there is deliberately empty:\nthe client nests peers under a user by matching `user_name` against an account\nname, and since no machine belongs to a particular account here, account rows\nwould only ever select down to nothing.\n\nAnything a machine reports is trusted, so **run this on a private network, a\nVPN, or behind a firewall**. Any client that can reach the URL will list itself.\n\n## Quick start\n\n### Local\n\n```sh\ncargo build --release\n./target/release/rustdesk-api user add alice     # prompts for a password\n./target/release/rustdesk-api serve\n```\n\n### Docker\n\n```sh\ndocker compose up -d --build\ndocker compose exec rustdesk-api rustdesk-api user add alice\n```\n\nThe database lives in the `rustdesk-api-data` volume. For a first account\nwithout a shell, set `RDAPI_ADMIN_USER` / `RDAPI_ADMIN_PASSWORD` in\n`docker-compose.yml` — they apply on first boot only and are ignored once any\nuser exists, so they cannot silently reset a password later.\n\n### Point the clients at it\n\nIn the RustDesk client: **Settings → Network → ID/Relay Server → API Server**,\nset to `http://your-host:21114`. Then **Settings → Account → Login**.\n\nWorkstations only need the API Server field set — they register themselves\nwithout anyone logging in. Machines you want to *appear* in the address book\nneed the setting; machines you connect *from* need it too, to log in.\n\n## Managing it\n\n```sh\nrustdesk-api user add alice --admin --email alice@example.com\nrustdesk-api user list\nrustdesk-api user passwd alice\nrustdesk-api user rm alice          # also drops their sessions + personal book\n\nrustdesk-api device list            # every workstation that has reported in\nrustdesk-api device rm 123456789    # returns if that machine reports again\n```\n\nUnder Docker, prefix with `docker compose exec rustdesk-api`.\n\nPasswords are hashed with Argon2id. Sessions are opaque 256-bit tokens stored\nserver-side, so `user rm` and `user passwd` take effect immediately.\n\n## Configuration\n\nAll via environment variables:\n\n| Variable | Default | Meaning |\n|---|---|---|\n| `RDAPI_BIND` | `0.0.0.0:21114` | Listen address |\n| `RDAPI_DB` | `sqlite://rustdesk-api.db` | SQLite path (`sqlite:///data/x.db` for absolute) |\n| `RDAPI_TOKEN_TTL_DAYS` | `30` | Session lifetime |\n| `RDAPI_DEVICE_STALE_DAYS` | `0` | Hide devices not seen in N days; `0` shows all |\n| `RDAPI_ADMIN_USER` | — | First-boot admin, ignored once any user exists |\n| `RDAPI_ADMIN_PASSWORD` | — | First-boot admin password |\n| `RUST_LOG` | `rustdesk_api=info,tower_http=warn` | Log filter |\n\n`RDAPI_DEVICE_STALE_DAYS` is worth setting if machines get reimaged often —\notherwise decommissioned workstations linger in the list until you\n`device rm` them.\n\n## Deployment notes\n\nThree client-side quirks that will waste your afternoon if you hit them blind:\n\n- **Plain HTTP is fine.** The client supports it, and defaults to it when\n  deriving the API URL from a custom ID server.\n- **Do not serve HTTPS on port 21114.** The client silently strips the port from\n  `https://host:21114` unless its `allow-https-21114` option is set. Use\n  `https://host` on 443 behind a reverse proxy, or plain HTTP on 21114.\n- **Do not host it under a `rustdesk.com` domain.** The client treats those\n  hosts as the public service and disables heartbeats entirely, so nothing would\n  ever register.\n\nSelf-signed certificates work — the client retries with verification disabled\nand caches that decision.\n\n## Exposing it to the internet\n\nRegistration (`/api/sysinfo`) is **unauthenticated** — the RustDesk client sends\nno credentials there, so anyone who can reach the URL can add a device to the\nshared address book. On a LAN that's the whole point; on the public internet it\nis not what you want.\n\nPut a reverse proxy in front (you need one anyway — the client can't do TLS on\n21114). `deploy/nginx.conf.example` and `deploy/Caddyfile.example` are complete,\ncommented examples that:\n\n- terminate **TLS** on 443 and proxy to the app on `127.0.0.1:21114`;\n- **rate-limit** `/api/login` against brute force;\n- restrict device registration to **allowed networks** (e.g. `192.168.1.0/24`\n  plus your VPN subnet), so strangers can't inject address-book entries.\n\nBoth belong at the proxy, not in the app: the proxy sees the real client IP\nunspoofably, whereas the app behind it only sees `127.0.0.1` unless it trusts an\n`X-Forwarded-For` header — which an attacker can forge. So keep IP allowlisting\nand rate limiting at the edge.\n\nSet `RDAPI_BIND=127.0.0.1:21114` so the app is reachable only through the proxy,\nnever directly. And remember the split: registration is locked to trusted\nnetworks, but `/api/login` and the address-book reads stay open to the world —\nthat is remote access working — so those are protected by rate limiting, strong\npasswords and TLS, not by the allowlist. A machine you want to be *controllable*\nmust therefore reach the server from an allowed network (i.e. over the VPN);\nmachines that only connect *out* don't register and are unaffected.\n\n## The logged-in connection bug (and the shim that fixes it)\n\nIf you self-host **OSS** hbbs, logging a client in to *any* API server — this one\nor another — breaks outgoing connections. Every attempt fails after exactly 18\nseconds with:\n\n```\nFailed to secure tcp: deadline has elapsed: Please try later\n```\n\nLog out and connections work again. This is an upstream client bug, not an\nAPI-server problem ([#13053](https://github.com/rustdesk/rustdesk/issues/13053),\n[#12875](https://github.com/rustdesk/rustdesk/issues/12875),\n[ProxmoxVE#12079](https://github.com/community-scripts/ProxmoxVE/issues/12079)).\n\n**Cause.** `src/client.rs:429` in client 1.4.9:\n\n```rust\nif !key.is_empty() \u0026\u0026 !token.is_empty() {\n    secure_tcp(\u0026mut socket, \u0026key).await   // waits READ_TIMEOUT = 18_000 ms\n}\n```\n\n`key` never *is* empty (it falls back to a built-in), so the condition reduces\nto \"is the user logged in?\". `secure_tcp` then waits for the rendezvous server\nto send a `KeyExchange` first — which only hbbs **Pro** implements. OSS hbbs\nwaits for the client instead, so both sides wait and the deadline expires. The\nrequest never leaves the machine, which is why the peer sees nothing at all and\nthe relay logs no request.\n\n**Fix.** `rendezvous-shim/` is a ~60-line, zero-dependency TCP shim. The client\ndoes not actually require encryption — a non-`KeyExchange` or even unparseable\ngreeting lands in its `_ =\u003e {}` arm and it proceeds unencrypted. So the shim\nwrites one 4-byte frame on connect and then pipes to hbbs.\n\nDeploy it next to hbbs (see the compose service and systemd unit in that\ndirectory). hbbs owns TCP *and* UDP 21116 and needs host networking to see real\nclient IPs, so the shim takes only inbound TCP via a DNAT rule:\n\n```\nclient --TCP 21116--\u003e [DNAT] --\u003e shim :21126 --\u003e 127.0.0.1:21116 (hbbs)\nclient --UDP 21116---------------------------\u003e  hbbs, untouched\n```\n\nThe control channel stays plaintext — exactly as it already is for logged-out\nclients. End-to-end encryption between peers is separate and unaffected.\n\n**Caveats.** It relies on the client tolerating a non-`KeyExchange` greeting; if\nupstream tightens `secure_tcp`, it stops working. It also corrupts RustDesk's\nrarely-used \"TCP proxy fallback\" (API calls tunnelled over 21116), which is\nalready broken against OSS hbbs anyway.\n\n**Alternative**, no server changes: set `allow-websocket = 'Y'` on every client.\n`secure_tcp` returns immediately when that is on. The cost is that WebSocket\nmode sets `force_relay` (`src/client.rs:1863`), so all traffic is relayed\ninstead of going peer-to-peer.\n\n## Tests\n\n```sh\n./tests/e2e.sh\n```\n\nBoots a real server on a temporary database and drives it with the exact request\nshapes a RustDesk client sends, asserting the details the client is strict\nabout: `type: \"access_token\"` verbatim, `currentUser` returning a *bare*\npayload, `total` on paged responses, `/api/ab/tags/{guid}` returning a bare\narray, successful mutations returning a *zero-length* body, `forceAlwaysRelay`\nas the string `\"false\"`, `SYSINFO_UPDATED` as literal text, and 401 only where\na client logout is actually intended.\n\n## Endpoints\n\nImplemented against the RustDesk 1.2.4+ protocol.\n\n**Account** — `POST /api/login`, `POST /api/logout`, `POST /api/currentUser`,\n`GET /api/login-options`\n\n**Device registration** (unauthenticated by design; the client sends no\ncredentials on these) — `POST /api/sysinfo`, `POST /api/sysinfo_ver`,\n`POST /api/heartbeat`\n\n**Address book** — `/api/ab/personal`, `/api/ab/settings`,\n`/api/ab/shared/profiles`, `/api/ab/peers`, `/api/ab/tags/{guid}`,\n`/api/ab/peer/add/{guid}`, `/api/ab/peer/update/{guid}`, `/api/ab/peer/{guid}`,\n`/api/ab/tag/add/{guid}`, `/api/ab/tag/update/{guid}`,\n`/api/ab/tag/rename/{guid}`, `/api/ab/tag/{guid}`\n\n**Accessible devices tab** — `GET /api/device-group/accessible`,\n`GET /api/users`, `GET /api/peers`\n\n**Accepted and discarded** — `/api/audit/conn`, `/api/audit/file`,\n`/api/audit/alarm`, `PUT /api/audit`\n\n**Operational** — `GET /health`\n\nRequests to anything else are logged at `WARN`, which is the first place to look\nif a future RustDesk release starts calling something new.\n\n## Not supported\n\nLegacy clients (1.2.3 and older) use a different address book protocol and will\nnot work. Shared-book write permissions, user groups, device groups, 2FA, OIDC\nand web management are all absent by design.\n\n## License\n\n[AGPL-3.0](LICENSE), matching upstream RustDesk. This is an independent\nreimplementation of the API the RustDesk client talks to — it contains no\nRustDesk source code — but AGPL is used to stay aligned with the ecosystem. If\nyou run a modified version as a network service, you must make your source\navailable to its users.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjjairola%2Frustdesk-api","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjjairola%2Frustdesk-api","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjjairola%2Frustdesk-api/lists"}