Skip to content

burakgon/linuxdrop

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

55 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

📋 LinuxDrop

Self-hosted, end-to-end encrypted clipboard sync + direct file transfer between Android and Linux.

License: Apache 2.0 Platforms Self-hosted

LinuxDrop does three things between your phone and computer — in real time, end-to-end encrypted, with no cloud account and no vendor in the middle:

  • 🔄 Clipboard sync — copy a link, an OTP, a paragraph, or an image on one device and it's instantly ready to paste on the other.
  • 📂 Direct file transfer — on any network. Send any file or photo straight device-to-device, whether the two are on the same Wi-Fi or on completely different networks (phone on mobile data, laptop across town). The bytes go peer-to-peer — full speed on a LAN, hole-punched directly over the internet otherwise — and never pass through a server.
  • 📶 Phone internet, one press. Laptop has no Wi-Fi? Tap Connect in the Linux tray and it wakes your paired phone over Bluetooth, turns on its hotspot, and joins it — you're back online through the phone's mobile data without ever touching the phone.

The relay that connects your devices is one you host yourself.

You run your own server. There is no built-in/default relay — nothing transits anyone else's infrastructure. Stand one up in a single command (see Self-hosting).

✨ Features

  • 🔄 Clipboard sync — text and images. Copy here, paste there, automatically (a screenshot on your phone is ready to paste on your desktop).
  • 📂 Direct P2P file transfer — works across any network. Files stream device-to-device over WebRTC: directly even when the two devices are on different networks (NAT hole-punched via STUN, no port-forwarding), and at full speed when they share a LAN. The bytes never touch the relay. Send from the Android Share sheet (or a device's send button), or right-click → "Send with LinuxDrop" on Linux.
  • 📶 Phone internet sharing — press-to-connect. When the computer is offline, one press in the Linux tray wakes your phone over Bluetooth, turns on its Wi-Fi hotspot, and joins it — online through the phone's mobile data, hands-off. The phone arms it with a single toggle (Shizuku-driven, no root); connect/disconnect stay manual, and the phone safety-offs the hotspot if the laptop walks away. The wake command is itself AES-sealed, so only your paired computer can ring the doorbell.
  • 🔒 End-to-end encrypted — AES-256-GCM with a key derived from a secret only your devices know.
  • 🕵️ Zero-knowledge relay — the server only sees an opaque room id; it can't read your clipboard, filenames, or device names, and file bytes never reach it.
  • 🔋 Battery-friendly & event-driven — no polling. On Android the clipboard is read in the background via Shizuku (no root); on Linux via wl-clipboard.
  • 🕘 History — recent clipboard items and transferred files are kept locally (encrypted); tap a clip to re-copy it, or a received file to open it.
  • 📷 QR pairing & multi-device — share a network by QR; many devices can join the same one.
  • 🔁 Resilient — auto-reconnects on network changes; survives sleep/roaming.
  • 🛡️ Yours alone — everything is E2E-encrypted between your own devices over your own relay (OTPs and passwords included — syncing those is the point); the server can read none of it.

🖼️ Screenshots

Onboarding Home History
Onboarding Home History

🧭 How it works

Android (Kotlin / Shizuku)                                   Linux (Go / wl-clipboard)
        │                                                              │
        │── clipboard (tiny E2E frames) ─►  your relay  ◄─ clipboard ──│
        │                                 room = hash(secret)          │
        └──────────  files: direct P2P (WebRTC, any network)  ────────┘
   Clipboard frames are AES-256-GCM end-to-end. Files stream straight between the two
   devices on any network — LAN-direct, or hole-punched over the internet via STUN —
   and never go through the relay.

A secret (32 random bytes) defines a sync network. Its hash becomes the roomId the relay routes by; an HKDF of it becomes the AES key your devices encrypt with. The relay is a thin, stateless pub/sub that never sees the secret — it relays encrypted clipboard frames and the small WebRTC signaling, while file bytes go peer-to-peer. Full spec: proto/PROTOCOL.md.

🚀 Self-hosting

You need a small box with Docker and a domain (or a Tailscale/WireGuard address). The bundled compose runs the relay behind Caddy, which provisions a Let's Encrypt certificate automatically — so wss:// works out of the box.

git clone https://github.com/burakgon/linuxdrop.git
cd linuxdrop/backend

# Point your domain's DNS at the host, then:
LINUXDROP_DOMAIN=relay.yourdomain.com docker compose up -d --build

That's it — your relay is live at wss://relay.yourdomain.com. Use that URL when you set up the first device; other devices receive it automatically from the pairing QR.

  • No public domain? Run it on a Tailscale/WireGuard network and use ws://<private-ip>:3000.
  • Cross-network transfer just works over STUN hole-punching — no port-forwarding. For the strictest NATs, flip on the bundled TURN relay (just below).
  • Already running nginx? See the advanced path in backend/README.md (docker-compose.prod.yml + deploy/nginx-relay.conf.example).
  • Verify it: bun scripts/relay-check.ts wss://relay.yourdomain.com.

Optional: TURN, for the strictest NATs

Direct hole-punching (STUN) already carries file transfers across most networks. For the rare symmetric-NAT / locked-down mobile case, the compose bundles a TURN relay (coturn) — opt in with a profile, so a plain docker compose up stays STUN-only and opens no extra ports:

export TURN_SECRET=$(openssl rand -hex 32)            # shared by the relay and coturn
export TURN_URL=turn:relay.yourdomain.com:3478
LINUXDROP_DOMAIN=relay.yourdomain.com docker compose --profile turn up -d --build

Open UDP 3478 and 49160-49200 in your firewall (on clouds with 1:1 NAT — GCP/AWS/Azure — also add --external-ip=<public-ip> to the coturn command). Clients then fetch short-lived TURN credentials from /ice automatically — no client-side change needed.

📱 Client setup

Linux (KDE/Wayland):

cd linux
bash install.sh   # builds linuxdropd → ~/.local/bin, installs the tray app, systemd unit,
                  # and the "Send with LinuxDrop" right-click action

linuxdropd pair <linuxdrop://… | hex> wss://relay.yourdomain.com   # pair to your relay
linuxdropd qr                                  # show a QR for your phone to scan
systemctl --user enable --now linuxdrop        # start syncing (system tray)

Android:

  1. Install the APK (grab it from Releases or build it — see android/README.md) and install Shizuku.
  2. Open the app → finish the guided Shizuku step.
  3. Enter your relay URL and Create network, or Scan QR from another device.

Sending files

  • Android: Share → LinuxDrop from any app, or tap the send icon next to a device. Received files land in Downloads and show in the in-app history (tap to open).
  • Linux: right-click a file in Dolphin → "Send with LinuxDrop", or the tray's "Send file…" (linuxdropd send [--to <device>] <file>… from the terminal). Received files land in ~/Downloads and the folder opens automatically.

Phone internet (tether)

  • On Android, turn "Phone internet sharing" on once — it arms the phone to answer over Bluetooth (keep the app allowed to run in the background).
  • On Linux, when the computer has no Wi-Fi, open the tray → "Connect to phone internet": it wakes the phone over Bluetooth, turns on its hotspot, and joins it. "Disconnect from phone" tears it down. (linuxdropd tether on|off|status does the same from the terminal.) Needs a working Bluetooth adapter on the computer and Shizuku on the phone.

🔐 Security model

  • The secret never reaches the relay. Pairing is offline (QR / linuxdrop:// link / hex).
  • roomId = base64url(SHA-256(secret))[:32]; encKey = HKDF-SHA256(secret, …); clipboard payloads are AES-256-GCM with a fresh random IV each time. A wrong secret fails the GCM tag → rejected.
  • File transfer is peer-to-peer (WebRTC, DTLS-encrypted). The WebRTC offer/answer is itself sealed with your key before it crosses the relay, so the relay can't MITM the connection — and the file bytes never reach it.
  • The relay stores only the last encrypted clipboard frame per room (reconnect catch-up) and short-lived encrypted blobs for clipboard images (room-scoped, ~30 min TTL). It can decrypt none of it.
  • Cross-language crypto is pinned by proto/crypto-test-vectors.json.

🛠️ Build from source

Component Stack Build
backend/ Bun + Hono + SQLite bun install && bun test · docker compose up -d --build
linux/ Go (+ wl-clipboard, pion/webrtc) go build ./... && go test ./...
android/ Kotlin + Compose + Shizuku + WebRTC bash scripts/build-apk.sh (hermetic Docker build)

📄 License

Copyright 2026 burakgon — licensed under Apache-2.0. The reflective IClipboard access pattern on Android is inspired by scrcpy (Apache-2.0).