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.
What it looks like
Section titled “What it looks like”Connecting a computer
Section titled “Connecting a computer”Open Computers (the laptop button in the header), click Get the command and run what it shows in a terminal on your computer:
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:
~/.cache/holtdroid/holtdroidor install the holtdroid command from Settings and just run
holtdroid.
The client’s screens
Section titled “The client’s screens”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,oor a second click opens one’s session page in the browser. Chrome’schrome://inspect/#deviceslists the WebViews of every device inadb devices(keep Discover USB devices on), with full DevTools;wor 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,javaand the like). Space publishes or unpublishes the one under the cursor,npublishes a port by number (also one nothing listens on yet),ashows every other listener too: system services and desktop apps. A port the gateway refused shows why. The Mode line on top (mor 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
holtdroidinto the first writable directory on yourPATHamong~/.local/bin,~/bin,/opt/homebrew/binand/usr/local/bin(~/.local/binotherwise, with the line to add it to yourPATH). The link points at~/.cache/holtdroid/holtdroid, whichtunnel.shmoves to the newest client whenever you run it again; an existingholtdroidthat isn’t this client is never replaced.
Updates
Section titled “Updates”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 onUpdate 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.
Publishing ports
Section titled “Publishing ports”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.
Publish modes
Section titled “Publish modes”- 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,viteand 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:
adb -s 127.0.0.1:15555 shellThe 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.
What is open right now
Section titled “What is open right now”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.
How it is secured
Section titled “How it is secured”- 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: Bearerwith 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.
Running the gateway
Section titled “Running the gateway”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 keysshGateway: enabled: true hostKey: env:SSH_HOST_KEY caKey: env:SSH_CA_KEYWithout 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.
Operator settings
Section titled “Operator settings”| 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.