I built a remote desktop for my Macs with no account, no cloud and no open ports
I wanted to reach my own Macs from my phone and my laptop. Every tool I tried wanted an account, sent my sessions through someone else’s servers, or both. For machines that hold everything I work on, that felt like the wrong trade. So I built OwnDesk: a Mac hosts, and another Mac, an Android phone or an iPhone controls it. There is no account, no cloud service and no port open to the Internet. It is open source under the MIT license, and v0.4.0 has a Mac download and an Android APK: github.com/im-fahad/OwnDesk This post is about the design decisions, because they are the interesting part. No server, so who connects the two devices? On the same network, nobody needs to. The Mac advertises itself over Bonjour, and the controller connects straight to a WebSocket the Mac serves. Away from home, I use Tailscale. The Mac has a stable tailnet address, Tailscale connects the two devices directly when it can, and relays when it cannot. OwnDesk never runs a server of its own. One detail made this work in practice. A Mac announces its Tailscale addresses in its Bonjour record, and every paired device that hears it at home keeps them. So a phone finds the Mac from a cafe even if the two were paired while Tailscale was off. The controller probes every address it knows at once and uses the first one that answers, so the same button works at home and away. Trust comes from one moment: pairing Pairing happens once, face to face, on the same network: The Mac shows a QR code holding its device id, its addresses and a one-time pairing code. The phone scans it and sends a pairing request with an HMAC proving it has the code, without sending the code itself. Both screens show a short fingerprint, like 25AA-F3B7-4F82. If they match, you click Approve on the Mac. That comparison is the whole security of pairing. After it, each side stores the other’s public key, and that key is the only thing that can ever start a session. The keys themselves never leave hardware. On a Mac and an iPhone they live in the Secure Enclave, and on Android in the Keystore. Both support ECDSA P-256 and neither supports Ed25519, which decided the curve. A device’s id is simply the SHA-256 of its public key, so the id proves which key it belongs to and nothing has to be registered anywhere. Signing the session, not trusting the network Every message that sets up a session is a signed envelope, checked in a fixed order: size, version, recipient, known sender, signature, clock skew, replay, and finally the JSON Schema. Any failure is final, and most failures get no reply at all. The part I like most is how this protects the video itself. The stream is WebRTC, encrypted with DTLS-SRTP. Normally the weak point is the SDP exchange: if someone in the middle swaps the DTLS fingerprint in the SDP, they can sit in the middle of the encrypted stream. In OwnDesk the SDP travels inside a signed envelope, so the DTLS fingerprint is signed too. Swap it, and the signature fails. Neither the network nor a relay can impersonate a device or read the session. One protocol, three languages The Mac apps are Swift, the Android app is Kotlin, and there is a TypeScript reference implementation. Three implementations of a security protocol is three chances to disagree. So the protocol is defined once, as JSON Schema. Codegen produces the TypeScript types and the Swift key table from it, and a set of shared test vectors pins down the exact bytes an envelope signs, the pairing proof and sixteen receiver cases covering replay, tampering, clock skew and unknown senders. All three implementations run the same vectors. If the Android app disagrees with a vector, it disagrees with both Macs, and the test says so. The iPhone app has no protocol code at all: it links the Mac controller’s own core, so pairing, signing and reconnection are the same code a MacBook runs. A terminal, without a “run command” message I also wanted a shell on the Mac, the way Termius gives you one. The obvious way would be a message type that runs commands, and that is exactly what I refused to add. The protocol has no message that can run a command or touch a file. There is a list in the spec of messages that must never exist. Instead, the terminal is the Mac’s own SSH server with OwnDesk as the client. Each device has a second hardware-backed key just for SSH. The only thing OwnDesk carries for the terminal is a one-time request: “please let this key in”. The Mac shows who is asking, someone clicks Allow, and the Mac writes the key line itself, tagged with the device, then answers with its host keys so the device can pin them. Unpairing removes the line again. Clipboard sync, opt-in on both sides Clipboard sync is useful and dangerous: whatever anyone copies on the Mac, a password included, could reach a device. So it is off until the person at the device switches it on, and a Mac shares its own clipboard only with the devices it is told to, a per-device switch that starts off. What a device copies reaches the Mac without that switch, since it could type the same text anyway. And clipboard text is never logged. Things that cost me time macOS ties permissions to the code signature. I sign without an Apple certificate, and an ad-hoc signature changes on every build, so Screen Recording and Accessibility had to be granted again after every rebuild. The install script now clears the stale entries so the new build can ask. Full-size desktops fell back to VP8. At 1920x1200 the H.264 level offered by default is too low for the frame size, so the controller’s offer now puts H.264 first and states level 5.2. Phones only read the clipboard when they are in front. On Android and iOS, clipboard sync sends what you copied when you come back to the app, not the moment you copy it. Testing on real devices is where the bugs were. Unit tests passed while a scripted drag on a real phone was landing one row too low. Every feature now has an end-to-end script that drives the real app against a real host. Where it falls short It is early, and I would rather say so: Only Macs can be controlled. Phones only control. No file transfer or audio yet. The Mac app is signed without an Apple certificate, so you confirm it once after downloading. The iPhone app must be built from source with your own Apple ID. Try it Download: github.com/im-fahad/OwnDesk/releases The design and threat model: docs/spec.md in the repository Issues and pull requests are welcome. I would especially like feedback on the security design. If you find a hole in it, please report it privately through GitHub’s security advisories rather than in public.