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?
- 0 · Before you open anythingTermux installed from F-Droid,
opensshandmoshinstalled 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. - 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.
- 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.
- 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. - 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.
- 2 · The name resolves inside the tunnelMagicDNS answers
macbookfrom within the tailnet, not from whatever DNS the café hands you. Save the MagicDNS name, never the100.xaddress — the name survives a device being removed and re-added, and the address does not. - 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 reasonping macbookfails on an Android and never on an iPhone. - 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.
- 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 genuinemoshclient, so this rung is as strong as it is on the iPhone. - 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 phoneattaches rather than starting anything. Setting battery usage to Unrestricted is the entire reason this scenario differs from the other two. - 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.
- 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-lockdoes 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.
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
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. Settings → Network & internet → VPN → Tailscale → ⚙. 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 Settings → Network & internet → Private 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:
- Give it a passphrase. Use
ssh-agentin 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. - Give it its own entry in
authorized_keyson every host, with a comment that names the device. You want to revoke this one key without touching the iPhone's. - Constrain it to the tailnet, so it is useless from anywhere else even if a server is later exposed.
- 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
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.
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.
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.
| Flavour | What to change | Notes |
|---|---|---|
| Pixel / stock | Settings → Apps → app → Battery → Unrestricted | 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:
#!/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:
- 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.
- 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.
- Are you using Mosh, or did you fall back to SSH? The command is
mosh, notssh. - Did the name fail to resolve rather than the session dropping? Check Private DNS.
ping macbookfailing with unknown host whileping 100.x.y.zworks is that fault exactly. - 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 moshcompleted.- Tailscale installed, signed in, approved in the admin console.
- Always-on VPN enabled for Tailscale.
- Private DNS set to Off or Automatic, and
ping macbookresolves. - 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/configentry 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.