Перейти до вмісту

Concepts

Connecting your computer

Publish your computer's ports on your devices and reach their ADB with the holtdroid client.

Цей контент ще не доступний вашою мовою.

holtdroid is a small client you run on your own computer. While it runs, every device you have running in the portal reaches the ports you pick as http://localhost:<port>, and each device’s ADB gets a port on your computer. It works over SSH through the portal’s gateway, but you don’t need SSH keys or an ssh command.

holtdroidzsh
Meanwhile, in another terminal, adb already knows the device.

Open Computers (the laptop button in the header), click Get the command and run what it shows in a terminal on your computer:

Terminal window
curl -fsSL https://android.example.com/tunnel.sh | sh -s -- eyJhbGciOiJIUzI1NiIs…

The script finds the client build for your system (macOS or Linux, Intel or Arm), downloads it from the portal, checks its SHA-256 and runs it with the code from the command. The client then:

  • makes its own key for this portal (it never touches ~/.ssh) and trades the code for this computer’s enrollment, named after its hostname;
  • connects to the gateway and opens its full-screen UI;
  • keeps running until you press q, and reconnects by itself when the network drops or the laptop wakes up.

The code works once and only for 10 minutes, so a command in your shell history is useless afterwards. Next time, run the client without a code:

Terminal window
~/.cache/holtdroid/holtdroid

or install the holtdroid command from Settings and just run holtdroid.

The sections are on the left; ↑/↓ or 1–5 pick one, → or Enter goes into it, ← or Esc comes back, q quits.

The mouse works too: click a section or a hint in the bottom line (y yes, …) to do what its key does, and scroll lists with the wheel. One click on a port or a setting acts on it; a device opens on the second click. While the client has the mouse, hold Option (macOS) or Shift (most Linux terminals) to select text.

The client doesn’t manage devices: launching, restarting, reinstalling and stopping them, logcat, a shell and the device’s details are on the portal’s session page. Launch a device (n or a click, on Overview and Devices) opens the portal in the browser.

  • Overview: whether the computer is connected, as whom, how many devices run, what is published and how long the certificate is valid.

  • Devices: your running devices with their local ADB address and whether they are already in adb devices; Enter, o or a second click opens one’s session page in the browser. Chrome’s chrome://inspect/#devices lists the WebViews of every device in adb devices (keep Discover USB devices on), with full DevTools; w or a click on the plate under the devices opens it in Chrome. With ADB turned off on the portal the plate says it’s unavailable.

  • Ports: the published ports and the ports of programs that look like dev servers (node, bun, python, java and the like). Space publishes or unpublishes the one under the cursor, n publishes a port by number (also one nothing listens on yet), a shows every other listener too: system services and desktop apps. A port the gateway refused shows why. The Mode line on top (m or a click switches it) picks how ports get published, see Publish modes.

  • Log: what the client did, newest at the bottom.

  • Settings: the client’s version, the portal, user, computer and key, where the enrollment and the binary are, and Install: it links holtdroid into the first writable directory on your PATH among ~/.local/bin, ~/bin, /opt/homebrew/bin and /usr/local/bin (~/.local/bin otherwise, with the line to add it to your PATH). The link points at ~/.cache/holtdroid/holtdroid, which tunnel.sh moves to the newest client whenever you run it again; an existing holtdroid that isn’t this client is never replaced.

On every start the client asks the portal which builds it serves and compares their SHA-256 with its own. When the portal has a different one, the client offers to update:

holtdroid A newer client is available from android.example.com.
▸ Update now and start again
Skip keep this one; it may not work correctly with the portal
[ ] Update automatically from now on

Update now downloads the portal’s build into ~/.cache/holtdroid, checks its SHA-256, points ~/.cache/holtdroid/holtdroid (and so the holtdroid command) at it and starts it with the same arguments. With the box ticked it updates without asking from then on; the setting is in Settings under Updates, next to Update now while this client is outdated, and is kept in holtdroid/client.json in your user config directory. Skip keeps the running client; the Overview then warns that it may not work correctly. Without a terminal an outdated client only prints a warning, unless automatic updates are on. --no-update-check skips the check.

Without a terminal (in a script, a service or with --plain) the client prints the banner and a short status block instead, whenever the connection, the devices or the published ports change.

Options: --portal <url> when the computer is enrolled with several portals, --publish 3000,5173 to replace the list of published ports, --publish-mode manual|dev|all to switch the publish mode, --no-adb-connect to leave adb alone, --plain for status lines instead of the full-screen UI, --insecure to accept a self-signed certificate of the portal (remembered for that portal), -v for debug logs. The key and the enrollment are in your user config directory (~/Library/Application Support/holtdroid/connect on macOS, ~/.config/holtdroid/connect on Linux); delete the portal’s folder there to forget it.

The client watches which TCP ports listen on your computer’s loopback and reports them to the portal. Publish them in the client’s Ports section or on the Computers page, which lists each connected computer’s published ports and dev servers (Show all adds the other listeners); click a port to publish it on your devices, click again to stop. You can also publish a port nothing listens on yet, for a dev server you are about to start. The choice is saved on the computer, so it survives restarts of the client.

  • Manual: only the ports you pick.
  • Dev servers (the default): also every port of a program that looks like a dev server (node, bun, python, java, vite and the like), as soon as it starts listening, and until it stops.
  • Everything: also every other listener on the loopback, system services and desktop apps included.

The ports you picked stay published in every mode. The client’s TUI asks before it switches to an automatic mode: anything on your devices, including any page in a WebView, then reaches those services as localhost. The automatic modes never publish the adb server (5037), local emulators or another holtdroid client, which would let a device control other devices; the client’s own ADB ports aren’t listeners it reports in the first place. The mode is saved per portal on the computer. The Computers page shows it and greys out the ports it published; to stop one of those, switch back to Manual in the client.

A published port goes to every device you run, including ones you start later, and only to yours. Apps and WebViews reach it as http://localhost:<port>. Plain HTTP to localhost, 127.0.0.1 and ::1 is allowed even when the app’s network security config forbids cleartext traffic; every other host still follows the app’s policy. Ports are bound on the device’s loopback only, so 80 and 443 are fine on emulators. If another connection already published a port on a device, it is skipped there and tried again later.

Android in an emulator session shares its network with the session’s own programs, so their ports can’t be published there: adb (5555, 5038), Frida (27042), the agent (18580, 18581), the VPN sidecar (7890, 9090) and the metrics port when metrics are on.

Every running device gets its own port on your computer, from 15555 up, kept for as long as the client runs. If adb is on your PATH, the client runs adb connect for each device and adb disconnect when it stops, so they just show up in adb devices:

Terminal window
adb -s 127.0.0.1:15555 shell

The Computers page and the SSH card next to a session’s screen show which port belongs to which device. On emulators adb connect needs no authorization prompt and adb shell is root (redroid is a userdebug build and the agent restarts adbd as root), with the node’s kernel underneath. adb reboot, adb unroot, adb remount, adb tcpip and adb usb are refused: they would cut the session off from its device. For logcat, files and a shell without any of this, use the ADB tools on the session page.

The SSH card next to a session’s screen shows the ADB clients and published ports of that device. While its panel is open, each port is checked through its own tunnel: green means your server answers, red means nothing listens on that port on your computer. Clicking a port copies http://localhost:<port> (http://localhost for 80, https://localhost for 443). The Computers page also lists every device you have tunnels to, and the laptop button in the header shows how many of your computers are connected.

  • The api is an SSH certificate authority. For the code (and later for the computer’s refresh token) it signs the computer’s public key into a certificate valid for 15 minutes, for your SSH user name only. The gateway trusts nothing but certificates from that authority, so it keeps no keys.
  • The client renews the certificate in the background, and each renewal also replaces the refresh token. A refresh token is tied to the computer’s key: someone who copies the token without the key gets nothing. A computer that isn’t used for 30 days (refreshTTL) has to be connected again with a new command.
  • The portal tells the client the gateway’s host key, so the client refuses a gateway that presents a different one.
  • For the portal’s API (launching and stopping devices, restarting apps) every renewal also brings an access token, valid as long as the certificate, that the portal accepts as Authorization: Bearer with the rights the user has in the browser.
  • The gateway runs no commands: shells, exec, SFTP, agent forwarding and forwarding to anything but your devices’ ADB are refused. It announces itself as OpenSSH and greets only enrolled computers.
  • Tokens are not stored on the server yet, so a single computer can’t be revoked. Until the persistent database lands, an administrator revokes every computer at once by changing sshGateway.caKey; a stolen key and refresh token work until the refresh token expires.

Traffic through a tunnel counts as activity of the device it went to, so a device you use only from your computer is not reaped as idle.

With the Helm chart, turn on sshGateway.enabled and point a TCP entry at the …-ssh-gateway Service on port 2222. You can use a LoadBalancer or NodePort (sshGateway.service.type), or an ingress TCP route such as a Traefik IngressRouteTCP on a dedicated entrypoint. Give it a stable host key and certificate authority key, PEM private keys referenced as secrets:

existingSecret: holtdroid # SSH_HOST_KEY and SSH_CA_KEY, each from ssh-keygen -t ed25519 -N '' -f key
sshGateway:
enabled: true
hostKey: env:SSH_HOST_KEY
caKey: env:SSH_CA_KEY

Without hostKey the gateway makes a new one on every restart, and without caKey the api makes a new authority on every restart; either way every computer has to be connected again. The chart wires the rest: the api finds every gateway replica through a headless Service, the gateway fetches the authority and your sessions from the api’s internal port, and the session NetworkPolicy lets only the gateway reach the agents’ tunnel port (18581). The api image carries the client builds and serves them with /tunnel.sh.

Setting Default Meaning
allowADB true Forward each device’s ADB to the client
allowReverse true Publish client ports on the devices’ localhost
reversePorts 1-65535 Device ports that may be published, e.g. 80,443,1024-65535
maxTunnelsPerConnection 32 ADB channels plus published ports one computer may hold
codeTTL 10m How long the code in the portal’s command works
certificateTTL 15m Lifetime of a computer’s certificate, renewed in the background
refreshTTL 720h How long an unused computer stays enrolled
insecureTLS false The command and the client accept the portal’s certificate unchecked

The client learns allowADB and allowReverse from the gateway when it connects. With ADB off it opens no local ADB ports, runs no adb connect and marks chrome://inspect as unavailable. With publishing off it keeps your chosen ports but doesn’t ask for them, and both the client and the Computers page say so.

insecureTLS is for a portal with a self-signed certificate, such as a development setup: the command becomes curl -fsSLk … and the client starts with --insecure. Anyone between the computer and the portal could then read and change its traffic, the enrollment code included, so leave it off wherever the portal has a real certificate. The SSH connection itself stays protected by the gateway’s host key either way.

Behind a forward-auth proxy (auth.mode: header), let /tunnel.sh, /connect/ and /api/v1/connect/enroll and /refresh through without a login, and requests that carry Authorization: Bearer: the portal checks those itself.

The gateway drops a client that stops answering its keepalives after about 30 s (keepaliveInterval, 15 s), which frees its ports for the next connection.