Chapter 07 · Build Android

The Android client

Android reaches the same tailnet, runs the same Mosh, and attaches to the same tmux session as the iPhone in chapter 06. Two things are different, and both matter: your private key cannot hide in hardware the way it can on iOS, and whether your session survives a pocket is decided by your handset vendor rather than by Android.

The chain is the same five links as the iPhone chapter, so the two read side by side. What changes is where they break. On iOS the interesting question was where does the key live. On Android the key answer is fixed — it lives in a file — and the interesting question moves to the last link: will this phone still be running your terminal in twenty minutes?

Which Android you are holding
WHAT HAS TO HAPPEN, IN ORDER WHAT THE PHONE IS DOING 1 · Bring the tunnel up Tailscale as an Android VpnService 2 · Resolve the name MagicDNS — and what overrides it 3 · Sign the handshake an ordinary key file, on every Android 4 · Start the session mosh over UDP, launched from Termux 5 · Twenty minutes in a pocket Doze, plus whatever your vendor added IF THIS PHONE IS LOST TONIGHT WHAT LIVES ON THIS PHONE Tailscale · a WireGuard node key, in the app's store Termux · ~/.ssh, your config, and shell history the SSH key · a file — there is no enclave option here known_hosts · the fingerprints you accepted on day one nothing listening · unless you started sshd yourself the vendor decides whether the session survives — it never changes what losing the phone costs you always-on, from boot one word: macbook a file, in Termux mosh · UDP 60001 still attached always-on, from boot one word: macbook a file, in Termux mosh · UDP 60001 put to sleep needs Autostart on Private DNS wins a file, in Termux mosh · UDP 60001 process killed an exportable key · rotate it on all three hosts Termux from F-Droid, and one tuned setting pkg install openssh mosh, and a ~/.ssh/config entry battery for Termux and Tailscale set to Unrestricted the command is mosh macbook -- tmux new -A -s phone the same apps, and nothing changed in Settings One UI ships with Put unused apps to sleep enabled every rung below behaves identically until the last this is the most common Android setup, by volume the same apps, on the most restrictive defaults HyperOS gates background work behind Autostart and this handset has Private DNS set to a provider two separate faults, on two separate rungs the VpnService is already up Always-on VPN raises it before you unlock the phone only tailnet addresses route through it, not all traffic the key icon is not an exit node unless you set one the tunnel comes up late, or not at all HyperOS blocks the app from starting itself on boot Always-on VPN is set, but Autostart overrides it you open Tailscale by hand and it connects fine macbook resolves to 100.x, on the phone MagicDNS answers inside the tunnel, not on cafe DNS the name survives a device being removed and re-added the 100.x address does not — never save that instead Private DNS and MagicDNS both want port 53 Private DNS set to a hostname takes every query a public resolver has never heard of macbook this is stock Android, not a Xiaomi behaviour the file is read into Termux's memory to sign a real ed25519 private key, on a real filesystem Android has a hardware keystore — OpenSSH ignores it this rung is identical on every Android, by design ssh authenticates, then hands over to mosh mosh-server starts, prints a one-time key, ssh exits the session moves to UDP and stops caring about IPs Termux runs the real mosh client, not an emulation you unlock, and the pane is where you left it Doze suspended the radio; mosh reconnects from the new IP tmux new -A -s phone attached rather than started Unrestricted battery is the whole reason this worked One UI moved Termux into Sleeping apps unused for a few days, so the OS deprioritised it the mosh client stopped; the server is still waiting reconnecting reattaches — you lost time, not work HyperOS reclaimed the RAM and killed Termux no warning, no notification, and no crash to read termux-wake-lock would not have helped — that is CPU only tmux on the far end saved the work itself
  1. 0 · Before you open anythingTermux installed from F-Droid, openssh and mosh installed inside it, a host entry in ~/.ssh/config, and — the one thing people skip — battery usage for both Termux and Tailscale set to Unrestricted. That last setting is what separates this scenario from the other two.
  2. 0 · Before you open anythingIdentical software, and nothing touched in Settings. One UI ships with Put unused apps to sleep on, which quietly moves anything you have not opened in a few days into a restricted bucket. Every rung below behaves exactly like the Pixel until the last one.
  3. 0 · Before you open anythingIdentical software again, on the most restrictive defaults shipping today. HyperOS gates background work behind a per-app Autostart toggle that is off by default, and this handset also has Private DNS pointed at a public resolver. Those are two independent faults on two different rungs.
  4. 1 · The tunnel comes up firstTailscale on Android runs as a VpnService, which is why the phone shows a key icon. Turn on Always-on VPN in Settings so the tunnel is up before you unlock. Only tailnet addresses route through it — it is not carrying your ordinary traffic unless you have picked an exit node.
  5. 1 · The tunnel never came upAlways-on VPN is set, and it still did not start, because HyperOS blocks the app from launching itself at boot until you enable Autostart for it specifically. Opening Tailscale by hand connects instantly, which is exactly what makes this so confusing to diagnose — the app works, it just never runs on its own.
  6. 2 · The name resolves inside the tunnelMagicDNS answers macbook from within the tailnet, not from whatever DNS the café hands you. Save the MagicDNS name, never the 100.x address — the name survives a device being removed and re-added, and the address does not.
  7. 2 · Private DNS took the query firstAndroid's Private DNS sends every lookup to a fixed DoT resolver, and that resolver has never heard of macbook. MagicDNS and Private DNS both want to be the answer for every name, and on Android they cannot both win. Set Private DNS to Off or Automatic while you use MagicDNS. This is stock Android behaviour and has nothing to do with Xiaomi — it is shown here only because it is the most common reason ping macbook fails on an Android and never on an iPhone.
  8. 3 · The key is a file, and that is the whole storyTermux runs ordinary OpenSSH, which reads an ordinary private key from an ordinary filesystem. Android has a hardware-backed keystore, and OpenSSH in Termux does not use it — there is no Android equivalent of Blink's Secure Enclave key. Anything that can read Termux's data can copy this key. This rung is identical on every Android in the picker, and it is the one real security downgrade against chapter 06.
  9. 4 · SSH starts it, then gets out of the waySSH authenticates, launches mosh-server, collects a one-time key, and exits. From then on the session is UDP and identified by that key rather than by your IP address — which is what lets it survive Wi-Fi to mobile data. Termux runs the genuine mosh client, so this rung is as strong as it is on the iPhone.
  10. 5 · Twenty minutes later, it is still thereDoze suspended the radio while the screen was off, and the phone probably changed network on the way home. Mosh reconnects from whatever IP it now has, and tmux new -A -s phone attaches rather than starting anything. Setting battery usage to Unrestricted is the entire reason this scenario differs from the other two.
  11. 5 · Twenty minutes later, the client had been sleptOne UI decided Termux was not in active use and moved it to a restricted bucket, so the mosh client stopped. The server side is untouched and still waiting, so reopening Termux and reconnecting reattaches you to the same tmux session. You lost the round trip, not the work — which is the whole argument for tmux on the far end.
  12. 5 · Twenty minutes later, the process is goneHyperOS reclaimed the memory and killed Termux outright. There is no notification, no crash log to read, and nothing on the phone that explains it. termux-wake-lock does not prevent this — it holds the CPU awake, it does not stop the OS reclaiming RAM. The only thing that saved the work was tmux running on the machine at the other end.
Switch between the three handsets and watch which rungs move. Rungs 3 and 4 never change — the key is always a file, and Mosh always works. Rungs 1, 2 and 5 are entirely at the mercy of settings your vendor chose for you, which is why the checklist at the end of this chapter is mostly Settings paths rather than commands. The bar at the bottom never moves either: on Android, losing the phone always costs you a key rotation.

Harden the device first

A phone that can open a shell on three machines deserves the same care as those machines.

  • Six-digit PIN minimum, ideally an alphanumeric password. Fingerprint and face unlock are convenience on top of it — the PIN is what actually protects the encryption keys.
  • Short screen timeout, and enable Lockdown (hold the power button → Lockdown). It disables biometrics until the PIN is entered again — useful in exactly the situation where someone can hold your phone to your face.
  • Check your security patch level under Settings → About phone, and check how long your vendor supports the model. This varies more on Android than anything else in this chapter — some vendors ship seven years of updates and some ship two.
  • Face unlock is not equal across vendors. On most handsets it is camera-based and materially weaker than a fingerprint. Do not rely on it to gate anything.
  • Be deliberate about sideloading. You are about to install Termux outside the Play Store, which is correct — but it means the "only install from the Play Store" habit no longer protects you. Install from F-Droid or the project's own GitHub releases, and nowhere else.

Install Termux from the right place

The Play Store build is deprecated and will not update

Termux's Play Store release is no longer maintained. Google Play's policies and Android's W^X enforcement — which blocks an app targeting a modern API level from executing files in its own home directory — made that build untenable, and the project moved to F-Droid and GitHub as its only official sources. The builds are signed with different keys, so Android will refuse to install one over the other: if you already have the Play Store version, back up what you need, uninstall it, then install fresh.

# Inside Termux, once installed from F-Droid or GitHub
pkg update && pkg upgrade
pkg install openssh mosh

# optional, but useful later
pkg install termux-api

Tailscale on Android

Install Tailscale from the Play Store or F-Droid and sign in with the same account as your other machines. Then four settings, in this order:

  • Always-on VPN. SettingsNetwork & internetVPNTailscale. This is the Android equivalent of connect-on-demand, and it is what makes the tunnel available before you open a terminal.
  • Leave "Block connections without VPN" off unless you specifically want a kill switch. With it on, losing the tunnel loses all networking, which makes diagnosing anything harder.
  • Turn Private DNS off or back to Automatic at SettingsNetwork & internetPrivate DNS while you rely on MagicDNS. A Private DNS hostname captures every lookup on the device, and a public resolver cannot resolve your tailnet names.
  • Approve the device in the admin console if you enabled device approval on the tailnet.
# From Termux, once Tailscale is up
ping -c1 macbook
# then check the path from the Mac side
tailscale ping android

The key question, answered honestly

Chapter 06 was built around one choice: put the iPhone's private key in the Secure Enclave so it has no exportable form. There is no equivalent move on Android with this toolchain. Termux runs standard OpenSSH, which reads a standard key file. Android's hardware keystore exists and is genuinely good, but OpenSSH inside Termux does not talk to it.

So the Android key is a file that can be copied. You cannot fix that here — you can only reduce what it is worth:

  1. Give it a passphrase. Use ssh-agent in Termux so you are not retyping it constantly, but do not skip it — the passphrase is the only thing standing between a copied file and a working key.
  2. Give it its own entry in authorized_keys on every host, with a comment that names the device. You want to revoke this one key without touching the iPhone's.
  3. Constrain it to the tailnet, so it is useless from anywhere else even if a server is later exposed.
  4. Treat it as lower-trust than the iPhone key when you write your key custody notes in chapter 10. It is not equivalent, and pretending otherwise is how a threat model goes stale.
# In Termux — passphrase-protected, and named for the device
ssh-keygen -t ed25519 -C "android-termux" -f ~/.ssh/id_ed25519

# On each machine, constrained to the tailnet
from="100.64.0.0/10" ssh-ed25519 AAAAC3Nza... android-termux

Verify the host key on first connection

ssh yourname@macbook

The first connection asks you to trust a host key. Compare the fingerprint against what the server reports before accepting — this is the one moment where a man-in-the-middle would be invisible:

# Run this ON the server, compare to what Termux shows
for f in /etc/ssh/ssh_host_*_key.pub; do ssh-keygen -lf "$f"; done

Save a host so you type one word

~/.ssh/config, inside Termux
Host macbook
  HostName macbook.your-tailnet.ts.net
  User yourname
  Port 2222
  IdentityFile ~/.ssh/id_ed25519
  ServerAliveInterval 30
  ServerAliveCountMax 6

Use the MagicDNS name, not the 100.x address — names survive a device being removed and re-added.

Mosh and tmux — the daily command

mosh macbook -- tmux new -A -s phone

Identical to the iPhone. Mosh moves the session to UDP so it stops caring about your IP address, and tmux new -A attaches to the existing session if there is one and creates it if there is not. Connecting is attaching.

Make the shell usable with thumbs

Termux's extra-keys row gives you Ctrl, Alt and arrows without a hardware keyboard — configure it in ~/.termux/termux.properties. Then turn on tmux's mouse mode (chapter 04), which makes panes tappable and scrollback draggable. On a hardware keyboard, remap Caps Lock to Ctrl in Android's physical keyboard settings.

The battery problem

This is the part of Android that has no iPhone equivalent, and it is the reason most people give up on Android as a remote-dev client. Two separate mechanisms are at work, and they are often confused with each other:

  • Doze and App Standby are stock Android. They suspend network access and defer background work when the screen is off. Mosh handles this well — the session resumes when the radio does.
  • Vendor process-killing is not stock Android. It is an extra layer each OEM adds on top, and it terminates your app outright to reclaim memory. Mosh cannot survive this, because the client process no longer exists.
What termux-wake-lock does and does not do

termux-wake-lock holds a CPU wake lock so Termux keeps getting scheduled. It is worth running, and it does not stop an OEM from killing the process to free RAM. Those are different mechanisms, and only the settings below address the second one.

Notes per flavour

Apply these to both Termux and Tailscale. Paths drift between versions — the names are the thing to search for, not the exact menu depth.

FlavourWhat to changeNotes
Pixel / stock SettingsAppsappBatteryUnrestricted Behaves the way the Android documentation describes. One setting is usually enough.
Samsung One UI Battery → Unrestricted; remove the app from Sleeping apps; turn off Put unused apps to sleep The sleeping-apps list is the one people miss — an app can be Unrestricted and still be asleep.
Xiaomi HyperOS / MIUI Autostart on; Battery saver → No restrictions; lock the app in Recents (long-press the card → padlock) The most restrictive defaults shipping. Settings can reset after an OS update — re-check them. The Recents lock is the single most effective one.
OnePlus / OPPO / realme (ColorOS) Allow background running; disable battery optimisation; lock in Recents Behaves much like Xiaomi. Some builds add a separate "auto-launch" toggle.
GrapheneOS / CalyxOS Battery → Unrestricted. Check the per-app Network permission is granted. No vendor process-killing, so rung 5 behaves like stock. The extra permission model is the thing that surprises people, not the battery.

If your handset is not listed, dontkillmyapp.com maintains per-vendor instructions and is kept more current than any guide can be.

Reconnect without ceremony

Termux's Boot add-on runs scripts from ~/.termux/boot/ at startup, which is the tidiest way to hold a wake lock from the moment the phone comes up:

~/.termux/boot/wake-lock.sh
#!/data/data/com.termux/files/usr/bin/sh
termux-wake-lock

Make it executable with chmod +x. This helps with Doze. It does not help with a vendor that kills the process — nothing on the phone does, which is why tmux lives on the far end.

If sessions still drop

Work through it in this order — it is ordered by how often each is the actual cause:

  1. Is battery usage set to Unrestricted for both Termux and Tailscale? This is the answer most of the time, and it has to be set per app.
  2. Check the vendor list above. On Samsung, check Sleeping apps. On Xiaomi, check Autostart and lock the app in Recents. These are separate from the battery setting and are not implied by it.
  3. Are you using Mosh, or did you fall back to SSH? The command is mosh, not ssh.
  4. Did the name fail to resolve rather than the session dropping? Check Private DNS. ping macbook failing with unknown host while ping 100.x.y.z works is that fault exactly.
  5. Is tmux running on the far end? If it is, a drop costs you a reconnect and nothing else, and the question becomes cosmetic.

A useful split: if the server logs Connection reset by peer, the phone died. If the server logs nothing at all and the session simply goes quiet, the network died and Mosh will resume on its own. The diagnostic ladder in chapter 12 covers the rest.

Checklist

  • PIN or password set, short screen timeout, security patch level checked.
  • Termux installed from F-Droid or GitHub — not the Play Store build.
  • pkg install openssh mosh completed.
  • Tailscale installed, signed in, approved in the admin console.
  • Always-on VPN enabled for Tailscale.
  • Private DNS set to Off or Automatic, and ping macbook resolves.
  • Key generated with a passphrase, named for the device.
  • Public key added to every machine with a from= restriction and an identifying comment.
  • Host key fingerprints verified on first connection.
  • A ~/.ssh/config entry per machine.
  • Battery set to Unrestricted for Termux and Tailscale.
  • Vendor-specific settings applied from the table above.
  • Screen off for twenty minutes mid-session tested; the session reattached.
  • Wi-Fi → mobile data mid-session tested; the session survived.

The commands in this guide change firewall and login settings, and can lock you out of a machine. Practise on something disposable first. Everything here is provided as is, with no warranty — you accept the risk of running it. Read the disclaimer.