One connection, from click to close
Nothing here is a black box. This is the actual path a session takes through the system, including the two places it can be refused and the one place it can be ended.
What happens, in order
Five steps. The interesting one is the fourth.
Hand out a branded client
The installer already knows your rendezvous server, your relay, your API and your key — and that login is required.
The configuration is signed with your key at release time and verified by the client at startup, so a bundle that has been tampered with is discarded rather than trusted.
A person signs in
The client's account UI is upstream's; the backend answering it is yours. A token lands on the device and travels with every connection it makes.
The rendezvous connection is encrypted before the token crosses it — the server offers its signed key exchange unprompted, which upstream's own server never does.
The device registers to an owner
Registration is tied to an account. A device nobody owns is told so with a code the client already understands.
Registration deliberately does not fail closed: if it did, a backend outage would take every device offline at once and leave the emergency path nothing to reach.
A connection is authorized, or it is not
Before the two peers are introduced, the rendezvous server asks your API: this user, this device, this kind of connection — yes or no?
A no is returned as a sentence the user sees verbatim. A yes carries a permission set and an audit reference to the controlled device, in fields the protocol already has.
The session is logged — and can be ended
Connection and file-transfer events land against one session record. The console can close that session, or revoke the grant behind it.
Termination rides the heartbeat the controlled device already sends, so it reaches direct peer-to-peer sessions that never touch the relay.
POST /api/internal/authorize
{
"token": "<the signed-in user's token>",
"from_id": "482913604",
"to_id": "771204338",
"conn_type": "remote_desktop"
}
200 OK
{
"allow": true,
"audit_ref": "c2f1…",
"permissions": { "clipboard": false, "file_transfer": false }
}One request, one decision. The client retries a slow connect up to three times, so the rendezvous server caches the answer — otherwise a single connect would produce three decisions and three audit references, two of them describing sessions that never existed.
Allow carries more than a yes
An approval hands the controlled device two extra things, in fields the protocol already had and upstream never used.
Allow. The two peers are introduced, the grant’s permission set travels with the introduction, and an audit reference ties every later event to this one session.
Deny. No introduction happens, and the reason is shown to the person verbatim — “Please sign in to connect to this device.” — rather than a timeout they will file a ticket about.
Five components, three of them upstream's
The client
Rust core and a Flutter UI — upstream's, with a login gate added
Capture, codecs, input and transport are untouched. What we added is a flag that makes login mandatory, gates on the two places a connection starts, and a registration gate in the service process. With the flag off, the binary behaves exactly like upstream's.
hbbs — rendezvous
Introduces two peers, and is the sole enforcement point
It already checked the licence key, that the device exists, and that it registered recently. We added the question that matters: may this signed-in person reach this device? Both of its connection paths ask it.
hbbr — relay
Copies bytes between two streams. Deliberately unmodified
It has no concept of a user, a device or a session, and it only ever sees the fallback path. Leaving it alone keeps one enforcement point instead of two half-enforcement points.
The auth API
Decisions, accounts, grants, audit, disconnects
A small Fastify service over MongoDB. It answers the rendezvous server's authorization call, serves the endpoints the client already knows how to speak, and is the only thing the console talks to.
The admin console
A browser client, and nothing more
It renders no data on the server and holds no secrets. Its route guards are navigation, not security — the boundary is on the API, which re-reads the user from the database on every request.
What listens where
Six ports, and one of them is not for the internet. The API's number is not arbitrary — a client that is not told otherwise derives it from the rendezvous port.
| Port | Protocol | Service | Purpose | Exposure |
|---|---|---|---|---|
| 21114 | TCP / HTTP | apps/api | Auth API. The client derives this as rendezvous port − 2 unless api-server is set. | Public (put TLS in front of it) |
| 21115 | TCP | hbbs | NAT-type test listener — and, from loopback, the runtime console. | Not for the internet |
| 21116 | TCP + UDP | hbbs | Rendezvous. UDP registers and keeps alive, TCP carries punch-hole and relay signalling. | Public, both protocols |
| 21117 | TCP | hbbr | Relay fallback for peers that cannot punch a hole. | Public |
| 21118 | TCP | hbbs / client | WebSocket rendezvous; also the client's direct-access port. | Public if you need web clients |
| 21119 | TCP | hbbr | WebSocket relay. | Public if you need web clients |
Read the rest of it
The architecture notes, the deployment guide and the patch map are all in the repository, written for the person who has to operate this.