AndroControl is a secure remote control application that allows you to control your Linux system's mouse and keyboard from your Android smartphone
- Mouse Control: Move cursor, left/right/middle click, double-click, drag, and scroll
- Keyboard Control: Full QWERTY keyboard with modifier keys (Ctrl, Alt, Shift, Super/Win)
- Special Keys: Function keys (F1-F12), navigation keys, Tab, Escape, etc.
- Key Combos: Support for keyboard shortcuts like Ctrl+C, Alt+Tab, etc.
- Mutual TLS: All communication is encrypted with TLS 1.2/1.3, and each device authenticates with its own client certificate (no shared or copyable token)
- Certificate Pinning: QR-paired servers are pinned from the QR's fingerprint (no trust-on-first-use window); manual setup falls back to fail-closed TOFU
- Pairing & Revocation: A one-time enrollment token pairs a new device; devices can be revoked individually from the server
- Single-session control: Only one device controls the host at a time; other devices are refused without disturbing the active session
- QR Code Setup: Scan a QR code from the server for easy connection setup
- Multiple Servers: Save and manage multiple server configurations
- Haptic Feedback: Configurable vibration feedback for button presses
- Heartbeat: Connection health monitoring with automatic reconnection
![]() Touchpad & controls |
![]() On-screen keyboard |
![]() System keyboard (Type) |
![]() Server management |
![]() Settings · device & theme |
![]() Settings · scroll bar & haptics |
The solution follows a client-server architecture:
- Backend (Go): A lightweight server running on the target Linux system that receives control signals over TLS-encrypted TCP and executes them using uinput
- Frontend (Android): A Material Design app that sends mouse and keyboard inputs to the server
- Linux system with uinput kernel module
- Go 1.21+ (for building from source)
- Android 6.0 (API 23) or higher
- Camera permission (for QR code scanning)
- Network access to the server
-
Install uinput kernel module:
sudo modprobe -i uinput
To load on boot, add
uinputto/etc/modules. -
Build and run the server:
cd Backend-GO go build -o AndroControl ./AndroControl -
First run: The server will:
- Generate TLS certificates (stored in
certs/) - Generate an enrollment (pairing) token (stored in
auth_token) - Display a QR code for easy mobile setup
- Start listening on port 5050
- Generate TLS certificates (stored in
./AndroControl [flags]
-addr string Bind address (default "0.0.0.0"; use 127.0.0.1 for loopback only)
-port int TCP port to listen on (default 5050)
-data-dir string Directory with certs/, auth_token, devices.json (default: current dir)
-log-level string Log verbosity: debug, info, warn, error (default "info")
-show-qr Reprint the pairing QR (token + cert fingerprint) and exit
-list-devices List paired devices and exit
-revoke <id|name> Revoke a paired device by ID or name, then exit
-revoke-all Revoke all paired devices, then exit
-rename <id> -name <n> Rename a device, then exit
-prune-inactive <days> Remove devices not seen in N days, then exit
-cleanup Remove revoked devices from the registry, then exit
The server stops cleanly on Ctrl+C / SIGTERM, releasing the virtual input devices.
- Build the APK from source or download a release
- Install on your Android device
- Pair via QR code (recommended):
- Open the app and tap the QR scanner icon
- Scan the QR code displayed by the server (or reprint it with
androcontrol-ctl qr) - The app pins the server's certificate from the QR and pairs automatically
- Or pair manually:
- Add a new server with IP, port, and the pairing token
- Verify the certificate fingerprint shown on first connection (trust-on-first-use)
On first connection the app presents the pairing token once and the server records this device's client-certificate fingerprint; the pairing token is then discarded on the phone. Afterwards the device is recognized by its certificate (mutual TLS) and can be revoked independently from the server.
When running as a service, use the androcontrol-ctl helper — it runs the
command as the service user against the right data directory and reloads the
service for you:
sudo androcontrol-ctl qr # reprint the pairing QR (to add a new device)
sudo androcontrol-ctl regen-token # rotate the enrollment token + reprint the QR
sudo androcontrol-ctl list # list devices (id, name, status, last seen, IP)
sudo androcontrol-ctl revoke <id-or-name> # revoke one device (by ID or name)
sudo androcontrol-ctl revoke-all # revoke every device
sudo androcontrol-ctl rename <id> <name> # rename a device
sudo androcontrol-ctl prune-inactive <days> # drop devices not seen in N days
sudo androcontrol-ctl cleanup # drop revoked devices from the registryTo pair an additional device after first setup, run androcontrol-ctl qr to
reprint the QR (it contains the enrollment token and the server's certificate
fingerprint) and scan it from the app.
The NixOS module installs androcontrol-ctl automatically. For the plain systemd
deploy, install it once:
sudo install -m755 Backend-GO/deploy/androcontrol-ctl /usr/local/bin/.
Under the hood these map to the binary's own flags, which you can also call directly (e.g. when running the server by hand):
./AndroControl -data-dir /var/lib/androcontrol -list-devices
./AndroControl -data-dir /var/lib/androcontrol -revoke <id-or-name>
./AndroControl -data-dir /var/lib/androcontrol -revoke-all
./AndroControl -data-dir /var/lib/androcontrol -cleanup
./AndroControl -data-dir /var/lib/androcontrol -regen-tokenDevice records are stored in devices.json (certificate fingerprints only — no private
keys or secrets). Revoked devices are also pruned automatically once a day while the
server runs.
Why a helper? Admin commands edit
devices.jsonon disk while the running server holds the registry in memory.androcontrol-ctlreloads the service (sendsSIGHUP) so the change applies without a restart — and on reload the server immediately drops any live connection belonging to a revoked device. Doing it by hand is equivalent to: run the flag as the service user, thensudo systemctl reload androcontrol.
Every session is logged with an [AUDIT] prefix — auth_ok/pair_ok on connect and
disconnect (with session duration) on close. The server is sandboxed and can't reach a
desktop session, so notifications are delivered from the user side by watching that
journal:
- NixOS: set
services.androcontrol.desktopNotifications = true;— adds a per-user service that popsnotify-sendon connect/disconnect. - Any systemd distro: install
Backend-GO/deploy/androcontrol-notifyandandrocontrol-notify.service, thensystemctl --user enable --now androcontrol-notify.
Both need your user to be able to read the service journal (group
systemd-journal/wheel/adm). For headless push instead (ntfy/webhook/email), point a
journal-watcher at curl/mail rather than notify-send.
Bidirectional clipboard sharing with the phone while connected (opt-in: Settings → Clipboard sync in the app). Copy on the desktop and paste on the phone, or vice versa.
Because the sandboxed server can't reach the display server's clipboard, the server only
relays clipboard text over a loopback channel, and a small per-user desktop agent
(androcontrol-clip) does the actual clipboard access — wl-clipboard on Wayland or
xclip on X11.
- NixOS: set
services.androcontrol.clipboardSync = true;— enables the relay and adds a per-user agent service. - Any systemd distro: start the server with
-clip-port 5051, then installandrocontrol-clip(built binary) andandrocontrol-clip.serviceandsystemctl --user enable --now androcontrol-clip(needswl-clipboardorxclip).
On Android 10+ the OS only lets an app touch the clipboard while focused, so:
desktop→phone copies arrive instantly when the app is foreground, or as a tap-to-copy
notification when it's backgrounded; phone→desktop syncs automatically while the app is
foreground, or via the Send clipboard button on the connection notification.
Text only, 1 MB cap, and clipboard contents are never logged. The loopback relay is
loopback-only; set a shared ANDROCONTROL_CLIP_TOKEN on both server and agent to also
guard against other local users.
Send files between the phone and the desktop while connected (opt-in: Settings → File transfer), up to 5 MB per file.
- Android → desktop: in any app, Share → "Send via AndroControl". The file lands
in the desktop's
~/AndroControl/received/. - Desktop → Android:
androcontrol-ctl send <file…>(NixOS) — or, running the binary directly,androcontrol-clip send <file…>— or a file-manager action (seedeploy/thunar-send-action.md). The file is saved toDownloads/AndroControl/on the phone.
The receiving side confirms each transfer before it's written (a notification on the
phone, a zenity/kdialog dialog on the desktop); folders and multi-file shares are
zipped automatically. Files travel on a dedicated mTLS data port (separate from the
input/control stream) and are rate-paced so a transfer doesn't lag the cursor.
- NixOS: set
services.androcontrol.fileTransfer = true;(opens the data port and enables the agent's transfer role). - Any systemd distro: start the server with
-data-port 5052(open that port in your firewall), and run the agent withANDROCONTROL_FILE_TRANSFER=1(needswl-clipboard/xclipfor clipboard andzenity/kdialogfor the accept prompt). Desktop→phone send requires an active connection (the server can't dial the phone).
The 5 MB cap is a deliberate limit: larger transfers over flaky Wi-Fi can stall, and resumable transfers aren't implemented yet.
- Swipe on the touchpad to move the cursor
- Tap for left click
- Long press for right click
- Double tap for double click
- Two-finger swipe for scrolling
- Use the L/M/R buttons for click and drag operations
- Tap the Keyboard button to show the keyboard panel
- Use the on-screen QWERTY keyboard or the native keyboard
- Toggle Ctrl/Alt/Shift/Win modifiers for key combinations
- Special keys: Tab, Esc, Arrow keys, Home, End, Delete, F1-F12
While connected, the app runs a foreground service (with an ongoing notification and a "Disconnect" action) and holds a partial wake lock, so the session stays alive when the app is backgrounded or the screen is off. Allow the notification permission when prompted so the status is visible.
- Settings → This device shows this install's name and device ID.
- To unpair, open a server's Edit dialog and tap Unpair. If you're connected, the server revokes this device's certificate immediately. If you're offline, connect first (or revoke the device on the server) — the certificate identity is shared across servers, so there's nothing per-server to remove locally.
All communication between the app and server is encrypted using TLS 1.2 or 1.3. The server generates a self-signed certificate on first run.
The QR code embeds the server's SHA-256 certificate fingerprint, so a QR-paired server is pinned immediately — no trust-on-first-use window. For manual setup the app falls back to TOFU: it shows the fingerprint for you to verify, saves it, and verifies it on every later connection (warning on change). Pinning is fail-closed — the app never silently trusts an unverified certificate.
Repeated failed pairing/authentication attempts from an IP are rate-limited: after
5 failures within 5 minutes the IP is locked out for 15 minutes. All pairing and
authentication events are written to the log with an [AUDIT] prefix.
Each device has its own client certificate whose private key is generated in the Android Keystore (non-exportable, hardware-backed where available). The device proves its identity during the mutual-TLS handshake — no bearer token is sent or stored that could be copied or replayed.
- Pairing: the server generates a long-lived enrollment (pairing) token on
first run. To pair, a device (presenting its client cert) sends the enrollment token
once; the server records the device's certificate fingerprint in
devices.json. - Subsequent connections: the server recognizes the device by its certificate at the TLS handshake — pairing/tokens are not involved again.
- Devices can be revoked individually (
-revoke <id>); the cert is then rejected at the handshake and any live connection is dropped. - The registry stores certificate fingerprints only (the cert is public; the private key never leaves the device), plus last-seen time/IP for auditing.
Anyone with the enrollment token can pair a new device, so treat the QR code / token
as a secret and rotate it with androcontrol-ctl regen-token (a running service picks
up the new token automatically; existing paired devices keep working) if it leaks.
The authenticated TLS connection is the trust boundary. Idle connections are closed by a server-side read deadline, and the client sends periodic heartbeats (PING/PONG) to keep an active connection alive and detect drops.
The server allows only one active control session at a time. While a device is
connected, a connection attempt from a different device is rejected (AUTH:BUSY)
without disturbing the active session. The same device reconnecting (e.g. after a
network drop or app restart) reclaims its own slot, displacing the stale connection.
For the full trust model, hardening controls and the vulnerability-disclosure process, see SECURITY.md.
- Default port: 5050
- Protocol: TCP with TLS
- Both devices must be on the same network (or have appropriate routing)
- Firewall must allow TCP traffic on the configured port
Security note: anyone who can reach the port and holds a valid credential (a paired client certificate, or the enrollment token to pair one) gets full keyboard/mouse control of the machine. Run AndroControl only on trusted networks, bind to
127.0.0.1and use a VPN/SSH tunnel for remote access, or restrict the port with a firewall. Avoid exposing it directly to the internet.
Sample units are in Backend-GO/deploy/:
# 1. Load uinput at boot and grant the 'input' group access to it
sudo cp Backend-GO/deploy/uinput.conf /etc/modules-load.d/uinput.conf
sudo cp Backend-GO/deploy/99-uinput.rules /etc/udev/rules.d/99-uinput.rules
sudo modprobe uinput
sudo udevadm control --reload-rules && sudo udevadm trigger
# 2. Create a dedicated user and install the binary + helper + data dir
sudo useradd --system --no-create-home --groups input androcontrol
sudo install -Dm755 Backend-GO/AndroControl /usr/local/bin/AndroControl
sudo install -m755 Backend-GO/deploy/androcontrol-ctl /usr/local/bin/androcontrol-ctl
sudo install -d -o androcontrol -g androcontrol /var/lib/androcontrol
# 3. Install and start the service
sudo cp Backend-GO/deploy/androcontrol.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now androcontrol
sudo journalctl -u androcontrol -f # view the QR code / token / fingerprint# 4. (optional) Desktop notifications on connect/disconnect — run as your DESKTOP user
# (must be able to read the service journal: group systemd-journal / wheel / adm)
install -Dm755 Backend-GO/deploy/androcontrol-notify ~/.local/bin/androcontrol-notify
install -Dm644 Backend-GO/deploy/androcontrol-notify.service ~/.config/systemd/user/androcontrol-notify.service
systemctl --user daemon-reload && systemctl --user enable --now androcontrol-notifyThis repo is a flake with a NixOS module that creates the user, loads uinput, installs
the udev rule + androcontrol-ctl, and runs the hardened service.
# flake.nix
{
inputs.androcontrol.url = "github:Arana-Jayavihan/AndroControl";
# in your nixosConfigurations.<host>.modules:
# inputs.androcontrol.nixosModules.default
}# host configuration
services.androcontrol = {
enable = true;
port = 5050;
openFirewall = true; # or keep false and open the port elsewhere
# bindAddress = "127.0.0.1"; # loopback only (reach via VPN/SSH tunnel)
desktopNotifications = true; # per-user notify-send popups on connect/disconnect
clipboardSync = true; # bidirectional clipboard sync (per-user agent)
fileTransfer = true; # file transfer (≤5 MB), opens the data port
};Manage devices with sudo androcontrol-ctl … (it's on PATH automatically). The pairing
QR / token / fingerprint appears in journalctl -u androcontrol on first start, or via
sudo androcontrol-ctl qr.
cd Backend-GO
go build -o AndroControlcd Frontend
./gradlew assembleDebugThe APK will be in Frontend/app/build/outputs/apk/debug/.
AndroControl/
├── Backend-GO/ # Go server
│ ├── main.go # Entry point, handshake, command loop, uinput
│ ├── auth.go # Enrollment-token authentication
│ ├── devices.go # Per-device registry (pairing/revocation)
│ ├── auththrottle.go # Failed-attempt lockout
│ ├── logging.go # Leveled logging
│ ├── tls.go # TLS certificate handling
│ ├── protocol.go # Command protocol
│ ├── validation.go # Input validation
│ ├── ratelimit.go # Rate limiting
│ ├── qrcode.go # QR code generation
│ ├── connmanager.go # Connection management
│ └── deploy/ # systemd unit, udev rule, androcontrol-ctl, androcontrol-notify
├── Frontend/ # Android app
│ └── app/src/main/java/com/aranaj/androcontrol/
│ ├── MainActivity.java # Main UI and controls
│ ├── ConnectionService.java # Foreground service (background persistence)
│ ├── SettingsActivity.java # Settings + device info
│ ├── SettingsManager.java # Preferences + client device id
│ ├── TlsHelper.java # TLS/TOFU implementation
│ ├── SecureStorage.java # Encrypted storage
│ ├── Protocol.java # Communication protocol (pair/auth/unpair)
│ ├── HeartbeatManager.java # Connection health
│ ├── Server.java # Server model
│ ├── ServerManager.java # Server persistence
│ └── QRScannerActivity.java # QR code scanning
└── Assets/ # Screenshots and resources
- Ensure uinput module is loaded:
lsmod | grep uinput - Check if port 5050 is available:
ss -tlnp | grep 5050 - Run with sudo if uinput permissions are insufficient
- Verify both devices are on the same network
- Check firewall settings on the server
- Ensure the correct IP address and port are configured
- If you regenerated server certificates, clear the saved fingerprint in the app
- Go to server settings and delete/re-add the server
- Ensure the server has permissions to use uinput
- Check server logs for error messages
- App:
versionNameinFrontend/app/build.gradle.kts. - Wire protocol:
ProtocolVersioninBackend-GO/protocol.go(client and server negotiate compatibility on connect). - Releases are tagged
vX.Y.Z; pushing a tag builds and publishes the Linux server binaries and the Android APK, using the matchingCHANGELOG.mdsection as the release notes (see.github/workflows/release.yml).
Keep the app version and protocol version in step when changing the wire format. Notable changes are recorded in CHANGELOG.md.
Without signing secrets the release attaches a debug APK (installable, but debuggable). For a proper signed release APK, add these repository secrets (Settings → Secrets and variables → Actions) from your existing keystore:
| Secret | Value |
|---|---|
ANDROCONTROL_KEYSTORE_BASE64 |
base64 -w0 your.jks |
ANDROCONTROL_KEYSTORE_PASSWORD |
keystore password |
ANDROCONTROL_KEY_ALIAS |
key alias |
ANDROCONTROL_KEY_PASSWORD |
key password |
With those set, the release job runs assembleRelease signed with your key. Local
builds and Android Studio's own signing are unaffected (the Gradle config only signs
when ANDROCONTROL_KEYSTORE is present in the environment).
AndroControl is self-hosted and collects no data for the developers. See PRIVACY.md for the full policy (required for the Play Store listing).
See LICENSE file.
This software is provided as-is. Use at your own risk. The authors are not responsible for any damages or misuse of this application.





