Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

FeedsContactPrivacy© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
phone-deck — Self-hosted control deck and remote desktop for Linux workstations over Tailscale, featuring WebRTC streaming, voice control with local LLM, scene macros, virtual input, file transfer, and live system telemetry. | Kitploit
Tools/GitHubGitHub/smit-shah-gg/phone-deck
Remote Access Tool
GitHubsmit-shah-gg/phone-deck

phone-deck

Self-hosted control deck and remote desktop for Linux workstations over Tailscale, featuring WebRTC streaming, voice control with local LLM, scene macros, virtual input, file transfer, and live system telemetry.

View Repository
201323 months agoReviewed by Kitploit

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share
# phone-deck

Turn a spare Android phone (or any device with a browser) into a self-hosted
control deck and remote desktop for your Linux workstation — over your private
Tailscale network, with no third-party apps.

It started as "what do I do with an old phone?" and became a control plane for a
Hyprland rig: window/workspace control, scene macros, a push-to-talk voice router with a
local-LLM assistant, context-aware in-app controls, two-way audio + screen streaming, a
touch remote-desktop, a virtual keyboard/mouse, file transfer + phone→PC sharing, and live
telemetry — all served as an installed PWA with a phosphor-terminal look.

> **Note on portability.** This was built for one specific setup: **Arch-based Linux
> (Garuda) + Hyprland + PipeWire + NVIDIA + Tailscale**, with the
> [end-4 / illogical-impulse](https://github.com/end-4/dots-hyprland) dotfiles. The
> architecture is generic, but several integrations are environment-specific (monitor
> names, workspace bindings, the matugen theme path, `cpupower`/`nvidia-smi`). Treat
> it as a working reference to adapt, not a turnkey package.

---

## Features

The UI is a tabbed, dark, landscape web app (theme-synced to your desktop):

| Tab | What it does |
|---|---|
| **Workspaces** | Live per-monitor workspace grid (tap to switch), live window list (tap to focus), per-monitor DPMS toggle + `ddcutil` brightness |
| **Remote** | On-screen trackpad (drag/tap/two-finger) + virtual keyboard with modifiers & combos, via a kernel `uinput` device |
| **Modes** | One-tap scene macros — launch/close/arrange whole app layouts across monitors (e.g. "Work" / "Free") |
| **Voice** | Push-to-talk voice router (faster-whisper, CPU): lead-word lanes — `macro` (run a mode/command), `type` (dictate), `input` (phrase → key chord) — with **confirm-before-execute**, plus **"friday"**, a read-only local-LLM answerer (Ollama + Qwen) with self-hosted web search and live system-state awareness |
| **Audio** | Mic/speaker mute, volume, output **and** input device pickers, `playerctl` transport + cover art (tracks the active MPRIS player) |
| **Stream** | Bidirectional **WebRTC audio** (PC↔phone, with phone-as-mic and phone-only output) **+ screen video**, and **tap-to-control the streamed screen** = a real remote desktop |
| **System** | Performance-mode toggle, live **theme** switching, Tailscale status, top processes (tap to kill), lock, NetworkManager restart, suspend/reboot/poweroff |
| **Files** | Screenshot a monitor → view/download on the phone; drop a file phone→PC |
| **Config** | Edit the `commands.json` / `modes.json` (and context/voice) config from the phone (JSON-validated) |

Always on, above the tabs — a **context strip** that surfaces what's happening right now:
an active call (mute / jump-to it), media now-playing + transport, and in-app keyboard
controls for the focused app (YouTube / Brave / Teams), delivered *without stealing focus*.

Two more, beyond the tabs:

- **Send-to-Rig** — the PWA registers as an Android **share target**: share an image, some
  text, or a link from any app on your phone and it lands on the rig — images saved to a drop
  dir *and* placed on the clipboard as paste-ready PNG, text to the clipboard, a bare link opened.
- **Ambient Cogitator** — after a few idle minutes the deck becomes a phosphor instrument panel:
  needle-dial telemetry with peak-hold ("was it pegged while I was away?"), clock, now-playing.

Plus: on-screen numpad PIN login, screen wake-lock, fullscreen landscape PWA, and
brute-force lockout on the login.

---

## Architecture

```
   phone / laptop (browser PWA)
            │  HTTPS + WSS  (Tailscale-only)
            ▼
   tailscale serve  ──►  FastAPI web app   ── Unix socket ──►  deckd
   (real TLS cert)       (runs as your user)   (action names)   (runs as root)
                              │                                  │
              hyprctl · pactl · ddcutil · grim ·          fixed allowlist of
              wf-recorder · uinput · WebRTC               privileged commands
                                                          (cpupower, nvidia-smi,
                                                           systemctl, …)
```

Two processes:

- **`app/` — the web app** runs as your normal user. It does everything that doesn't
  need root: Hyprland control, audio, brightness, screen capture, virtual input,
  WebRTC streaming, file transfer.
- **`deckd/` — a tiny root helper** (stdlib only, no dependencies) for the few
  privileged actions. The web app never sends it shell strings — only **action names**
  from a fixed allowlist (`governor_performance`, `gpu_power_limit`, `suspend`, …),
  validated in [`deckd/actions.py`](https://github.com/smit-shah-gg/phone-deck/blob/main/deckd/actions.py) before anything executes. Even if
  the web app were fully compromised, the blast radius is exactly the allowlisted
  actions, with no argument injection. The socket is `root:<group>` mode `0660`.

### Security model

- Reached **only inside your tailnet** — `tailscale serve` exposes it on your
  MagicDNS name with a real Let's Encrypt cert; it never binds to `0.0.0.0`.
- **JWT** login (PIN → signed cookie), with exponential-backoff lockout after repeated
  failures.
- Privileged actions are isolated behind the allowlisted helper daemon.
- Uploads are basename-sanitized (can't escape the drop directory).

> The web app *can* run your configured shell commands and inject input as your user —
> it is, by design, a remote control for your machine. Keep it on your tailnet, behind
> the PIN, and don't expose it publicly.

---

## Requirements

- Linux with **Hyprland** (wlroots), **PipeWire** (with `pactl`/PulseAudio compat)
- **Python ≥ 3.11** and **[uv](https://docs.astral.sh/uv/)**
- **Tailscale** (with HTTPS certificates enabled for your tailnet)
- CLI tools used by various features (install what you want to use):
  `hyprctl`, `pactl` / `pw-record` / `pw-play`, `playerctl`, `ddcutil`,
  `wf-recorder`, `grim`, `cpupower`, `nvidia-smi`, `kitty` (or your terminal)
- Your user in the **`input`** group (for `/dev/uinput`) and **`i2c`** group (for `ddcutil`)
- `aiortc` + PyAV (installed via `uv`) for audio/video streaming

---

## Installation

```bash
git clone <your-repo-url> phone-deck
cd phone-deck
uv sync                          # creates .venv and installs dependencies
uv run python -m app.set_pin     # set your unlock PIN
```

### 1. The root helper (`deckd`)

Edit `systemd/deckd.service` first — set the paths and `DECK_SOCKET_GROUP` to a group
your user belongs to (usually your primary group):

```bash
sudo cp systemd/deckd.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now deckd
```

`deckd` runs the system Python (stdlib only — no venv needed).

### 2. The web app

Run it directly for development:

```bash
uv run uvicorn app.main:app --host 127.0.0.1 --port 8765
```

…or as a user service. Edit `systemd/phone-deck.service` paths first, then install it.
**Important:** on a setup where Hyprland is *not* launched via uwsm, the systemd
`graphical-session.target` never activates, so the user unit won't auto-start on login.
The reliable fix is to start it from Hyprland with the session environment imported —
add to your Hyprland autostart (e.g. end-4's `~/.config/hypr/custom/execs.conf`):

```bash
exec-once = systemctl --user import-environment WAYLAND_DISPLAY HYPRLAND_INSTANCE_SIGNATURE XDG_RUNTIME_DIR DBUS_SESSION_BUS_ADDRESS XDG_CURRENT_DESKTOP && systemctl --user start phone-deck
```

### 3. HTTPS over Tailscale

1. Enable certificates once: Tailscale admin console → **DNS → Enable HTTPS Certificates**.
2. Front the app with a real cert:

   ```bash
   sudo tailscale serve --bg 127.0.0.1:8765
   ```

3. Set `DECK_SECURE_COOKIES=1` in the web-app environment once HTTPS is live.

### 4. Install on the phone

Open `https://<your-host>.<your-tailnet>.ts.net/` in Chrome on the phone (it must be on
your tailnet), log in, then **⋮ → Install app**. It launches fullscreen, landscape-locked,
with the screen kept awake. HTTPS is required for the PWA service worker + wake-lock.

> Works from any tailnet device — laptops included (the touch surfaces use Pointer
> Events, so a mouse works too). The WebRTC stream is single-peer (one viewer at a time).

---

## Configuration

All runtime config lives in `~/.config/phone-deck/` and is editable from the **Config**
tab (JSON-validated, keeps a `.bak`). Changes take effect on the next action — no restart.

### `commands.json` — custom shell actions

```json
[
  { "id": "reload_wm", "label": "Reload WM", "run": "hyprctl reload" },
  { "id": "deploy", "label": "Deploy site", "run": "cd /srv/app && ./deploy.sh",
    "confirm": true, "timeout": 120 }
]
```

`run` may be a string (executed via `bash -lc`) or a list (argv, no shell). These run as
your user — it's a local, trusted file.

### `modes.json` — scene macros

Each mode is an ordered list of steps run via `hyprctl`:

- `launch  {cmd, workspace, match?, once?}` — open an app on a workspace. With `match`
  (window class) + `once`, it won't duplicate, and it relocates the window if it lands on
  the wrong workspace (needed for apps like browser PWAs whose window comes from an
  existing process).
- `close  {match, workspaces?}` — close windows of a class, optionally only on listed workspaces.
- `move   {match, workspace}` — move matching windows to a workspace.
- `focus  {workspace}` — switch a workspace into view (e.g. reset each monitor to its home ws).

### Hyprland workspace → monitor binding

Mode placement is deterministic only if your workspaces are pinned to monitors. Add
bindings to your Hyprland config (adjust workspace numbers and monitor names to yours):

```
workspace = 1, monitor:DP-3, default:true
workspace = 2, monitor:DP-3
# … etc
```

### Theme

The deck wears a **phosphor-terminal** skin with four live-switchable color profiles —
**green** (default), **amber**, **ice**, and **auto** (derives the phosphor hue from your
matugen wallpaper palette, `~/.local/state/quickshell/user/generated/colors.json`). Switch
it in the **System** tab; the choice persists in `~/.config/phone-deck/theme.json`.
`app/theme.py` serves it as `/theme.css`. The Ambient Cogitator's idle timeout and liturgy
line are configurable here too.

---

## How streaming works

Bidirectional WebRTC via **aiortc + PyAV**, bridged to PipeWire/Hyprland with subprocess
pipes (avoids fragile device-I/O bindings):

- **PC → phone audio:** capture the default sink's `.monitor` with `parec` → Opus.
- **Phone → PC mic:** receive the phone mic → a PipeWire null sink whose monitor apps
  select as an input ("Monitor of PhoneDeckMic").
- **Phone-only output:** route PC playback into a virtual sink so the speakers go silent
  while you listen on the phone; restored on stop.
- **PC → phone video:** `wf-recorder` captures a monitor → aiortc, with quality presets
  (540p / 720p / 1080p) to keep the bitrate sane over flaky links.
- **Touch remote desktop:** tap/drag on the streamed video → absolute cursor positioning
  (`hyprctl movecursor`) + `uinput` clicks. Tap = click, drag = move, long-press = right-click.

Constraints: ~150–300 ms latency; reliable only while the PWA is foreground (Android
suspends background tabs); use headphones to avoid echo; one streaming peer at a time;
a browser can't capture the phone's *own* app audio (mic only).

---

## Tech stack

- **Backend:** FastAPI + Uvicorn, WebSockets, aiortc/PyAV, python-evdev, psutil, PyJWT
- **Frontend:** HTML + Tailwind (CDN) + vanilla JS, PWA (manifest + service worker + wake-lock)
- **Helper daemon:** Python standard library only
- **Transport:** Tailscale (`tailscale serve` for HTTPS/WSS)

## Tests

```bash
uv run pytest
```

Covers the `deckd` allowlist, JWT + login lockout, PIN hashing, config-editor validation,
modes parsing, the streaming quality/command logic, and the input keymap. The live
WebRTC/PipeWire/Hyprland paths are validated by loopback during development.

## Project layout

```
app/          FastAPI web app
  main.py       routes + WebSockets
  hypr.py       Hyprland (snapshot, dispatch, event socket, cursor)
  audio.py      PipeWire device control + playerctl
  audio_rtc.py  WebRTC audio + screen video bridge
  hid.py        virtual keyboard/mouse via uinput
  modes.py      scene-macro engine
  brightness.py ddcutil
  grab.py       screenshot + file upload
  theme.py      matugen → CSS
  ...
deckd/        root helper (stdlib only) + action allowlist
systemd/      service units
tests/        pytest suite
```

## License

MIT — see `LICENSE` (add one before publishing if you want a different license).

## Acknowledgements

Built for [Hyprland](https://hyprland.org/), [PipeWire](https://pipewire.org/),
[Tailscale](https://tailscale.com/), [aiortc](https://github.com/aiortc/aiortc), and the
[end-4 dotfiles](https://github.com/end-4/dots-hyprland).
Download Tool