sreevarshan-xenoz/Linux-Link

Remote desktop for Linux with Wayland streaming + KDE Connect features over Tailscale. Pure Rust, <100ms latency, no port forwarding needed.

Rust

5

517 commits

updated Sep 24, 2026

See the code

See what people are saying

SourceMessageScoreDate

I'm building Linux ↔ Android streaming in Rust and the telemetry got out of hand (r/rust)

I've been working on Linux-Link for a while. The original idea was pretty simple: use an Android phone as an extension of a Linux machine and stream things between the two. Then I started trying to make it behave properly over real networks and somehow the telemetry became a whole project by…

0

Sep 30, 2026

README

Linux Link

Remote desktop for Linux with Wayland-native screen streaming + full KDE Connect feature set — all over Tailscale. No port forwarding required.

Rust License: MIT CI Issues Status

Target: Sub-100ms latency screen streaming + KDE Connect integration + zero-config Tailscale connectivity

Linux Link is a pure Rust remote desktop solution built specifically for Linux (Wayland/Hyprland). It combines:

  • Low-latency screen streaming — wlroots/PipeWire capture → FFmpeg H.264 encoding → QUIC transport → Android MediaCodec decode
  • Full KDE Connect feature parity — clipboard sync, file transfer, notifications, input control, battery info, remote file browsing
  • Tailscale-native connectivity — No manual port forwarding or NAT traversal; just pair and connect

Key Differentiators

FeatureLinux LinkRustDeskSunshine/MoonlightKDE Connect
Tailscale-native✅❌❌❌
Wayland-native✅⚠️ Partial⚠️ Partial✅
Hyprland optimized✅❌⚠️ Limited⚠️ Limited
Full KDE Connect✅⚠️ Partial❌✅
Screen streaming✅✅✅❌
Pure Rust✅✅❌ (C++)❌ (C++)
Open Source✅✅✅✅

Features

Screen Streaming

  • Native wlroots capture — direct zwlr_screencopy on Hyprland (no portal grant dialog), damage-driven variable frame rate; automatic fallback to PipeWire capture via XDG Desktop Portal on any other Wayland compositor, X11 grab last
  • FFmpeg H.264 encoding with persistent sidecar process for low latency
  • QUIC transport (datagram mode) with self-signed TLS certificates
  • Adaptive bitrate — one arbiter owns every rate change: a 2 Mbit/s courtesy ceiling while a WAN session rides a relay, the session's quality preset, and a loss response that backs the encoder off a congested link (20 % per congested tick, to a 1 Mbit/s floor) and walks it back up once the link goes clean
  • MediaCodec hardware decode on the native Android client via JNI
  • Single-window streaming (Hyprland) — pick any window on the phone and the server crops + re-encodes to just that window

KDE Connect Integration

  • Clipboard sync — bidirectional clipboard sharing via wl-clipboard
  • File transfer — send files from Android to Linux via KDE Share protocol
  • Remote file browsing — browse and navigate Linux directories from Android
  • Notifications — receive Android notifications on your Linux desktop
  • Notification reply — answer desktop notifications from the phone; the desktop records the reply, copies it to the clipboard and shows a confirmation
  • Find my device — ring the desktop from the phone, or ring the phone from the desktop
  • PIN pairing — 6-digit PIN pairing (shown on the desktop or printed by linux-link pair); unpaired connections are locked out — the control channel (TCP and v2/QUIC) and the video/input stream all enforce it — unless pairing_required = false
  • Privacy mode — block the desktop's physical keyboard+mouse while remote (EVIOCGRAB with a 10-min auto-release TTL), lock the desktop from the phone or its session notification; uinput remote input keeps flowing
  • View-only mode — one tap makes the phone a pure viewer: the server drops all remote input for the session (video, clipboard and HUDs keep working), so nothing slips through from a stale tap queue; re-arms automatically after a stream reconnect
  • Relay bandwidth courtesy — a WAN session riding a relay automatically drops to a conservative video bitrate (relay bandwidth is shared, not ours to saturate) and restores full quality the moment it punches through to direct; one tap on the "Full quality" toggle overrides the floor if you want every bit of it
  • Desktop audio control — adjust the desktop's volume, mute, and default output device (headphones/speakers) from the phone (wpctl/pactl under the hood)
  • Phone mic share — one tap turns the phone's microphone into a desktop input device ("Linux Link Mic" PipeWire source, Opus over the session stream): take calls on the PC from the phone; stays live in view-only mode and dies with the session
  • Wake-on-LAN relay — wake a sleeping desktop from WAN by asking an always-on Linux peer on its LAN to emit the magic packet ("Send Wake-on-LAN" in the app)
  • Input control — remote mouse/keyboard via trackpad gestures
  • Battery info — monitor Android device battery from Linux
  • Presenter mode — play/pause/next/previous from Android

Android Client

  • Native Kotlin app (Jetpack Compose, Material 3 dark theme)
  • Rust core reused via a JNI/UniFFI bridge (no Flutter)
  • Connection screen with peer discovery over Tailscale
  • Remote desktop with tap/drag/double-tap gesture input
  • Link status — connecting/LAN/WAN indicator on the video surface, with failure reasons and one-tap retry
  • Session resilience — QUIC keepalive + bounded idle timeout hold the link through NAT stalls; a wake-locked foreground service keeps screen-off sessions alive
  • Picture-in-picture — shrink the live session into a video-only PiP window (desktop aspect ratio, chrome hidden) and keep working in other apps; also enables DeX/docked windows
  • Blackout / pocket mode — black, touch-locked screen while the session keeps streaming: the phone and its video surface go secure (no screenshots or Recents leakage), brightness drops; double-tap or back to unlock
  • Localized UI — English, Spanish and Tamil; pick per-app language from the "Language" button on the connect screen (Android 13+)
  • File browser with local and remote file tabs
  • Settings with DataStore persistence

Server

  • CLI with start/stop/status/list/watch/connect/pair/capabilities commands
  • systemd service for auto-start on boot
  • TOML configuration with video quality presets (low/balanced/high)
  • 52 passing tests across core and server crates

Installation

# Install latest release
curl -fsSL https://raw.githubusercontent.com/sreevarshan-xenoz/Linux-Link/main/scripts/install.sh | bash

# Install specific version
curl -fsSL https://raw.githubusercontent.com/sreevarshan-xenoz/Linux-Link/main/scripts/install.sh | bash -s -- v0.1.0

# Non-interactive install (no prompts)
curl -fsSL https://raw.githubusercontent.com/sreevarshan-xenoz/Linux-Link/main/scripts/install.sh | bash -s -- --yes

# Install to custom prefix
curl -fsSL https://raw.githubusercontent.com/sreevarshan-xenoz/Linux-Link/main/scripts/install.sh | bash -s -- --prefix /opt

# Preview without changes
curl -fsSL https://raw.githubusercontent.com/sreevarshan-xenoz/Linux-Link/main/scripts/install.sh | bash -s -- --dry-run

Install Script Options

FlagDescription
--yes, -yNon-interactive; accept all defaults
--dry-runPreview actions without making changes
--verboseShow detailed debug output
--forceForce reinstall even if same version
--no-serviceSkip systemd service installation
--no-configSkip config file creation
--no-docsSkip documentation installation
--no-manSkip man page installation
--prefix PATHInstall prefix (default: /usr)
--check-updatesCheck if installed version is current
--list-versionsList all available releases
--statusShow installation status
--rollbackRoll back to previous version
--uninstallRemove installation

Management commands (after install):

linux-link status             # Tailscale status of this machine
linux-link sessions           # Recent streaming-session outcomes
linux-link bench --baseline bench/baselines   # Encode tail vs the committed baseline
linux-link capabilities       # KDE Connect capability sets in use
linux-link pair --grant 15m   # Time-boxed one-off support PIN
linux-link kick <id|prefix|peer-ip|all>   # Drop a live session

The update/rollback/uninstall verbs belong to the install script, not the binary:

./scripts/install.sh --status           # Installation info (prefix, version, service)
./scripts/install.sh --check-updates    # Compare installed version with GitHub
./scripts/install.sh --list-versions    # List published releases
./scripts/install.sh --rollback         # Return to the previously installed version
./scripts/install.sh --uninstall        # Remove the installation

Build from Source

# Clone the repo
git clone https://github.com/sreevarshan-xenoz/Linux-Link.git
cd Linux-Link

# Build the workspace
cargo build --release

# Run tests
cargo test --workspace

# Run lints
cargo fmt --check
cargo clippy --workspace -- -D warnings

AUR (Arch Linux)

# Using an AUR helper
yay -S linux-link

# Or manually
git clone https://aur.archlinux.org/linux-link.git
cd linux-link
makepkg -si

Usage

Start the Server

# Using the installed binary
linux-link start

# Or with cargo
cargo run --release --bin linux-link -- start

There is no --config flag: the server reads $XDG_CONFIG_HOME/linux-link/config.toml (default ~/.config/linux-link/config.toml) if present, and falls back to built-in defaults. -v/--verbose and RUST_LOG control logging.

Configuration

Copy the example config and customize:

mkdir -p ~/.config/linux-link
cp config.toml.example ~/.config/linux-link/config.toml
# ~/.config/linux-link/config.toml
control_port = 1716        # KDE Connect compatible
streaming_port = 4716      # QUIC streaming port
log_level = "info"         # trace/debug/info/warn/error
video_quality = "balanced" # low/balanced/high
pairing_required = true    # PIN-pair devices before the control channel or streaming serves requests
# allow_hevc = false       # let negotiating phones drive an H.265/HEVC stream (R4 C1)

systemd Service

The server captures the desktop session (Wayland/X11) and talks to PipeWire, so it runs as a systemd user service — a system unit started at boot could never see the graphical session. install.sh drops the unit into ~/.config/systemd/user/ and enables it for you; from a desktop session:

systemctl --user enable --now linux-link
systemctl --user status linux-link
journalctl --user -u linux-link -f

The unit is WantedBy=graphical-session.target, so it starts at login and stops with the session. To run it without a login session (headless), enable lingering first: sudo loginctl enable-linger $USER.

CLI Commands

CommandDescription
linux-link startStart the server daemon
linux-link stopStop the running daemon
linux-link statusShow connection status
linux-link listList available peers on tailnet
linux-link watchWatch for peer discovery events
linux-link connect <peer>Connect to a specific peer
linux-link pair [pin] [--grant 15m]Print a 5-minute pairing PIN for the phone (generate or set); --grant time-boxes the trust pairing stores (s/m/h/d, e.g. 45s, 15m, 2h, 1h30m) for one-off support sessions
linux-link unpair [device-id]Remove a paired device from the trust store (all if omitted)
linux-link sessions [--count N] [--json]Show recorded streaming-session outcomes (LAN/WAN-punched/WAN-relayed tally + recent log tail); each line carries the transport's own numbers — packets/bytes lost, datagrams sent, path changes, seconds rode a relay, peak congestion window, discovered path MTU (read off the path the session actually rides, on both transports) — and the p50/p90/p95/p99/max tails for the whole chain: rtt_*/enc_* measured on the desktop, dec_*/rnd_*/e2e_* reported back by the device doing the decoding, plus its reading of the path as phone_rtt/phone_lost; --json emits the retained per-session records as JSON lines for tooling and benchmark comparison
linux-link bench [--target software|vaapi] [--baseline PATH] [--record PATH]Run the pinned 720p encode clip on this machine; with --baseline, exit non-zero if a percentile regressed past --tolerance-pct (exit 3 means no baseline matches this host, which is a skip, not a pass)
linux-link capabilities [--json|--markdown]Show the negotiated protocol versions, transports, capture backends and codecs this build actually speaks, plus its KDE Connect capability sets
linux-link kick <id|prefix|peer-ip|all>Drop a live streaming session (R4 D2)

Capabilities Are Generated, Not Written

linux-link capabilities is the single source of truth for the four numbers and lists a reader otherwise has to trust prose for: the negotiated protocol version, which transports a given build carries, which capture backends exist and in what order Auto tries them, and which codecs can be encoded/decoded on each side. Every value is read from the constants and decision tables the wire uses — protocol::{HANDSHAKE_HELLO, ALPN_V1_STREAM}, protocol::v2::{ALPN_V2, V2_MIN_VERSION..}, streaming::capture::capture_attempts, VideoCodec::ALL, AudioConfig::default() — so a client-profile build reports capture: unavailable instead of an empty list, and a version bump appears in the report the moment it appears in the code.

linux-link capabilities              # human listing
linux-link capabilities --json       # for scripts
linux-link capabilities --markdown   # the source of docs/capabilities.md

docs/capabilities.md is that Markdown output, committed, and server/tests/capabilities_doc.rs fails when it stops matching the build — regenerate it in the same commit as the constant you changed.

The Encoder Benchmark Is Pinned, Not Improvised

linux-link bench exists because "it felt laggy today" is not a regression test. It encodes one generated 720p clip — 300 frames, 5 Mbit/s, veryfast, H.264 — and reports the per-frame encode distribution (p50/p90/p95/p99/max). The clip is a pure function of its frame index (scene_frame), so the workload is byte-identical between runs without committing a gigabyte of raw frames, and it mixes a static gradient with a moving block so the encoder cannot coast on zero-residual skip blocks and measure nothing.

linux-link bench --target vaapi --repeat 5           # measure this machine
linux-link bench --baseline bench/baselines          # grade against a committed record
linux-link bench --record bench/baselines            # add this machine's record

A baseline is only comparable to a run on the same host, backend, geometry, bitrate, preset and codec — anything else is reported as SKIP (exit 3), never as a pass, because encode throughput does not travel between CPUs. --repeat N keeps the fastest run: other work on the machine can only make an encode slower, and on a desktop with a load average near 6 the best of seven runs reproduced the quiet-machine number (p50 7 ms) while a single run reported 11 ms. Encode time is the half this measures; decode and render live on the phone. CI runs the same command on every push — see bench/README.md.

Man Page

man linux-link

Android Client

The Android client is a native Kotlin app in android/, bridged to the shared Rust core via JNI/UniFFI.

cd android
./gradlew assembleDebug   # Debug build
./gradlew assembleRelease # Release build

Requires JDK 17+ and Android SDK/NDK.

Architecture

┌─────────────────────────┐         Tailscale          ┌─────────────────────────┐
│   Android Client        │◄──── Encrypted P2P ────────│   Linux Server          │
│   (Kotlin + Rust FFI)   │        (Tailscale IP)      │   (Hyprland)            │
├─────────────────────────┤                            ├─────────────────────────┤
│  UI Layer (Compose)     │                            │  Rust Daemon (tokio)    │
│  ├── Connection Screen  │                            │  ├── Screen Capture     │
│  ├── Remote Desktop     │                            │  ├── FFmpeg H.264 Enc.  │
│  ├── File Browser       │                            │  ├── QUIC Transport     │
│  └── Settings           │                            │  └── Adaptive Bitrate   │
├─────────────────────────┤                            ├─────────────────────────┤
│  Rust Backend (FFI)     │                            │  KDE Connect Plugins    │
│  ├── Connection Mgr     │                            │  ├── Battery            │
│  ├── Video Decoder      │                            │  ├── Clipboard          │
│  ├── File Transfer      │                            │  ├── Notification       │
│  └── Input Handler      │                            │  ├── Share              │
└─────────────────────────┘                            │  ├── File Browse        │
                                                       │  └── Input              │
                                                       └─────────────────────────┘

Data Flow (Streaming):

PipeWire → BGRA Frame → FFmpeg H.264 → QUIC Datagram → MediaCodec → SurfaceView

Data Flow (Input):

Touch Gesture → Rust FFI → QUIC/TCP → KDE mousepad packet → enigo → Wayland

Project Structure

Linux-Link/
├── core/                   # Shared Rust library
│   └── src/
│       ├── protocol/       # KDE Connect protocol + connection handling
│       ├── streaming/      # Capture, encoder, QUIC transport, adaptive bitrate
│       └── tailscale/      # Tailscale integration
├── server/                 # Linux server daemon
│   └── src/
│       ├── plugins/        # KDE plugins (5 + file browse)
│       ├── cli.rs          # CLI argument parsing
│       ├── config.rs       # TOML configuration
│       ├── kde.rs          # Plugin registry + service setup
│       └── service.rs      # Main server loop
├── android/                # Android client (native Kotlin)
│   ├── app/                # Kotlin app module (UI, services, MediaCodec decode)
│   └── bridge/             # Rust cdylib crate (UniFFI/JNI bindings to core)
├── aur/                    # AUR packaging (PKGBUILD)
├── docs/                   # Design specs and plans
├── man/                    # Man pages
├── scripts/                # Install script
├── .github/workflows/      # CI/CD (build, test, release, audit)
├── CHANGELOG.md            # Project changelog
├── CONTRIBUTING.md         # Contributor guidelines
├── config.toml.example     # Example configuration
├── linux-link.service      # systemd service file
└── plan.md                 # Full development plan

Quality Gates

CheckStatus
cargo fmt✅ Pass
cargo clippy -D warnings✅ Pass (0 warnings)
cargo test --workspace✅ 52 tests pass
cargo check --workspace✅ Clean compilation

Contributing

Linux Link is actively developed and looking for contributors!

Prerequisites

  • Rust 1.80+ (edition 2024)
  • JDK 17+ and Android SDK/NDK (for the Kotlin client)
  • Tailscale (for testing)
  • FFmpeg (runtime and dev libraries — the server encodes in-process via ffmpeg-next), PipeWire, xdg-desktop-portal (for streaming)

Development Workflow

# Build + test
cargo build --workspace && cargo test --workspace

# Format + lint
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings

# Android build
cd android && ./gradlew assembleDebug

See CONTRIBUTING.md for full guidelines.

Roadmap

All 6 development phases and the RustDesk research round (R4) have landed in code: the native Kotlin client, the Rust↔JNI bridge, MediaCodec decode, and the streaming/transport/security stack are in main, and v0.1.0 is tagged and pushed.

What is not done is verification and hardening, so treat this as beta rather than a release:

  • cargo fmt --all -- --check, clippy -D warnings across every core feature profile and cargo test --workspace all pass from a clean checkout of main (verified 2026-09-24). CI now runs that matrix plus the Android build, and cargo audit can fail a run — but no workflow has executed yet, because the branch has to be pushed first.
  • On-device verification for the large majority of shipped Android features (see docs/device-verification-checklist.md)
  • Cross-network (cellular ↔ LAN) WAN test over iroh; the Tailscale path is device-verified
  • A GitHub Release with artifacts (v0.1.0 is a tag only)

The full backlog, with every item's verified status against this tree, is docs/roadmap-3000.md; how that backlog gets executed is docs/roadmap-execution-plan.md. See plan.md for the development journal.


Stars, forks, and PRs are welcome! If Linux Link sounds interesting to you, drop a ⭐ and say hi.

sreevarshan-xenoz/Linux-Link

Remote desktop for Linux with Wayland streaming + KDE Connect features over Tailscale. Pure Rust, <100ms latency, no port forwarding needed.

Rust

5

517 commits

updated Sep 24, 2026

See the code

See what people are saying

SourceMessageScoreDate

I'm building Linux ↔ Android streaming in Rust and the telemetry got out of hand (r/rust)

I've been working on Linux-Link for a while. The original idea was pretty simple: use an Android phone as an extension of a Linux machine and stream things between the two. Then I started trying to make it behave properly over real networks and somehow the telemetry became a whole project by…

0

Sep 30, 2026

README

Linux Link

Remote desktop for Linux with Wayland-native screen streaming + full KDE Connect feature set — all over Tailscale. No port forwarding required.

Rust License: MIT CI Issues Status

Target: Sub-100ms latency screen streaming + KDE Connect integration + zero-config Tailscale connectivity

Linux Link is a pure Rust remote desktop solution built specifically for Linux (Wayland/Hyprland). It combines:

  • Low-latency screen streaming — wlroots/PipeWire capture → FFmpeg H.264 encoding → QUIC transport → Android MediaCodec decode
  • Full KDE Connect feature parity — clipboard sync, file transfer, notifications, input control, battery info, remote file browsing
  • Tailscale-native connectivity — No manual port forwarding or NAT traversal; just pair and connect

Key Differentiators

FeatureLinux LinkRustDeskSunshine/MoonlightKDE Connect
Tailscale-native✅❌❌❌
Wayland-native✅⚠️ Partial⚠️ Partial✅
Hyprland optimized✅❌⚠️ Limited⚠️ Limited
Full KDE Connect✅⚠️ Partial❌✅
Screen streaming✅✅✅❌
Pure Rust✅✅❌ (C++)❌ (C++)
Open Source✅✅✅✅

Features

Screen Streaming

  • Native wlroots capture — direct zwlr_screencopy on Hyprland (no portal grant dialog), damage-driven variable frame rate; automatic fallback to PipeWire capture via XDG Desktop Portal on any other Wayland compositor, X11 grab last
  • FFmpeg H.264 encoding with persistent sidecar process for low latency
  • QUIC transport (datagram mode) with self-signed TLS certificates
  • Adaptive bitrate — one arbiter owns every rate change: a 2 Mbit/s courtesy ceiling while a WAN session rides a relay, the session's quality preset, and a loss response that backs the encoder off a congested link (20 % per congested tick, to a 1 Mbit/s floor) and walks it back up once the link goes clean
  • MediaCodec hardware decode on the native Android client via JNI
  • Single-window streaming (Hyprland) — pick any window on the phone and the server crops + re-encodes to just that window

KDE Connect Integration

  • Clipboard sync — bidirectional clipboard sharing via wl-clipboard
  • File transfer — send files from Android to Linux via KDE Share protocol
  • Remote file browsing — browse and navigate Linux directories from Android
  • Notifications — receive Android notifications on your Linux desktop
  • Notification reply — answer desktop notifications from the phone; the desktop records the reply, copies it to the clipboard and shows a confirmation
  • Find my device — ring the desktop from the phone, or ring the phone from the desktop
  • PIN pairing — 6-digit PIN pairing (shown on the desktop or printed by linux-link pair); unpaired connections are locked out — the control channel (TCP and v2/QUIC) and the video/input stream all enforce it — unless pairing_required = false
  • Privacy mode — block the desktop's physical keyboard+mouse while remote (EVIOCGRAB with a 10-min auto-release TTL), lock the desktop from the phone or its session notification; uinput remote input keeps flowing
  • View-only mode — one tap makes the phone a pure viewer: the server drops all remote input for the session (video, clipboard and HUDs keep working), so nothing slips through from a stale tap queue; re-arms automatically after a stream reconnect
  • Relay bandwidth courtesy — a WAN session riding a relay automatically drops to a conservative video bitrate (relay bandwidth is shared, not ours to saturate) and restores full quality the moment it punches through to direct; one tap on the "Full quality" toggle overrides the floor if you want every bit of it
  • Desktop audio control — adjust the desktop's volume, mute, and default output device (headphones/speakers) from the phone (wpctl/pactl under the hood)
  • Phone mic share — one tap turns the phone's microphone into a desktop input device ("Linux Link Mic" PipeWire source, Opus over the session stream): take calls on the PC from the phone; stays live in view-only mode and dies with the session
  • Wake-on-LAN relay — wake a sleeping desktop from WAN by asking an always-on Linux peer on its LAN to emit the magic packet ("Send Wake-on-LAN" in the app)
  • Input control — remote mouse/keyboard via trackpad gestures
  • Battery info — monitor Android device battery from Linux
  • Presenter mode — play/pause/next/previous from Android

Android Client

  • Native Kotlin app (Jetpack Compose, Material 3 dark theme)
  • Rust core reused via a JNI/UniFFI bridge (no Flutter)
  • Connection screen with peer discovery over Tailscale
  • Remote desktop with tap/drag/double-tap gesture input
  • Link status — connecting/LAN/WAN indicator on the video surface, with failure reasons and one-tap retry
  • Session resilience — QUIC keepalive + bounded idle timeout hold the link through NAT stalls; a wake-locked foreground service keeps screen-off sessions alive
  • Picture-in-picture — shrink the live session into a video-only PiP window (desktop aspect ratio, chrome hidden) and keep working in other apps; also enables DeX/docked windows
  • Blackout / pocket mode — black, touch-locked screen while the session keeps streaming: the phone and its video surface go secure (no screenshots or Recents leakage), brightness drops; double-tap or back to unlock
  • Localized UI — English, Spanish and Tamil; pick per-app language from the "Language" button on the connect screen (Android 13+)
  • File browser with local and remote file tabs
  • Settings with DataStore persistence

Server

  • CLI with start/stop/status/list/watch/connect/pair/capabilities commands
  • systemd service for auto-start on boot
  • TOML configuration with video quality presets (low/balanced/high)
  • 52 passing tests across core and server crates

Installation

# Install latest release
curl -fsSL https://raw.githubusercontent.com/sreevarshan-xenoz/Linux-Link/main/scripts/install.sh | bash

# Install specific version
curl -fsSL https://raw.githubusercontent.com/sreevarshan-xenoz/Linux-Link/main/scripts/install.sh | bash -s -- v0.1.0

# Non-interactive install (no prompts)
curl -fsSL https://raw.githubusercontent.com/sreevarshan-xenoz/Linux-Link/main/scripts/install.sh | bash -s -- --yes

# Install to custom prefix
curl -fsSL https://raw.githubusercontent.com/sreevarshan-xenoz/Linux-Link/main/scripts/install.sh | bash -s -- --prefix /opt

# Preview without changes
curl -fsSL https://raw.githubusercontent.com/sreevarshan-xenoz/Linux-Link/main/scripts/install.sh | bash -s -- --dry-run

Install Script Options

FlagDescription
--yes, -yNon-interactive; accept all defaults
--dry-runPreview actions without making changes
--verboseShow detailed debug output
--forceForce reinstall even if same version
--no-serviceSkip systemd service installation
--no-configSkip config file creation
--no-docsSkip documentation installation
--no-manSkip man page installation
--prefix PATHInstall prefix (default: /usr)
--check-updatesCheck if installed version is current
--list-versionsList all available releases
--statusShow installation status
--rollbackRoll back to previous version
--uninstallRemove installation

Management commands (after install):

linux-link status             # Tailscale status of this machine
linux-link sessions           # Recent streaming-session outcomes
linux-link bench --baseline bench/baselines   # Encode tail vs the committed baseline
linux-link capabilities       # KDE Connect capability sets in use
linux-link pair --grant 15m   # Time-boxed one-off support PIN
linux-link kick <id|prefix|peer-ip|all>   # Drop a live session

The update/rollback/uninstall verbs belong to the install script, not the binary:

./scripts/install.sh --status           # Installation info (prefix, version, service)
./scripts/install.sh --check-updates    # Compare installed version with GitHub
./scripts/install.sh --list-versions    # List published releases
./scripts/install.sh --rollback         # Return to the previously installed version
./scripts/install.sh --uninstall        # Remove the installation

Build from Source

# Clone the repo
git clone https://github.com/sreevarshan-xenoz/Linux-Link.git
cd Linux-Link

# Build the workspace
cargo build --release

# Run tests
cargo test --workspace

# Run lints
cargo fmt --check
cargo clippy --workspace -- -D warnings

AUR (Arch Linux)

# Using an AUR helper
yay -S linux-link

# Or manually
git clone https://aur.archlinux.org/linux-link.git
cd linux-link
makepkg -si

Usage

Start the Server

# Using the installed binary
linux-link start

# Or with cargo
cargo run --release --bin linux-link -- start

There is no --config flag: the server reads $XDG_CONFIG_HOME/linux-link/config.toml (default ~/.config/linux-link/config.toml) if present, and falls back to built-in defaults. -v/--verbose and RUST_LOG control logging.

Configuration

Copy the example config and customize:

mkdir -p ~/.config/linux-link
cp config.toml.example ~/.config/linux-link/config.toml
# ~/.config/linux-link/config.toml
control_port = 1716        # KDE Connect compatible
streaming_port = 4716      # QUIC streaming port
log_level = "info"         # trace/debug/info/warn/error
video_quality = "balanced" # low/balanced/high
pairing_required = true    # PIN-pair devices before the control channel or streaming serves requests
# allow_hevc = false       # let negotiating phones drive an H.265/HEVC stream (R4 C1)

systemd Service

The server captures the desktop session (Wayland/X11) and talks to PipeWire, so it runs as a systemd user service — a system unit started at boot could never see the graphical session. install.sh drops the unit into ~/.config/systemd/user/ and enables it for you; from a desktop session:

systemctl --user enable --now linux-link
systemctl --user status linux-link
journalctl --user -u linux-link -f

The unit is WantedBy=graphical-session.target, so it starts at login and stops with the session. To run it without a login session (headless), enable lingering first: sudo loginctl enable-linger $USER.

CLI Commands

CommandDescription
linux-link startStart the server daemon
linux-link stopStop the running daemon
linux-link statusShow connection status
linux-link listList available peers on tailnet
linux-link watchWatch for peer discovery events
linux-link connect <peer>Connect to a specific peer
linux-link pair [pin] [--grant 15m]Print a 5-minute pairing PIN for the phone (generate or set); --grant time-boxes the trust pairing stores (s/m/h/d, e.g. 45s, 15m, 2h, 1h30m) for one-off support sessions
linux-link unpair [device-id]Remove a paired device from the trust store (all if omitted)
linux-link sessions [--count N] [--json]Show recorded streaming-session outcomes (LAN/WAN-punched/WAN-relayed tally + recent log tail); each line carries the transport's own numbers — packets/bytes lost, datagrams sent, path changes, seconds rode a relay, peak congestion window, discovered path MTU (read off the path the session actually rides, on both transports) — and the p50/p90/p95/p99/max tails for the whole chain: rtt_*/enc_* measured on the desktop, dec_*/rnd_*/e2e_* reported back by the device doing the decoding, plus its reading of the path as phone_rtt/phone_lost; --json emits the retained per-session records as JSON lines for tooling and benchmark comparison
linux-link bench [--target software|vaapi] [--baseline PATH] [--record PATH]Run the pinned 720p encode clip on this machine; with --baseline, exit non-zero if a percentile regressed past --tolerance-pct (exit 3 means no baseline matches this host, which is a skip, not a pass)
linux-link capabilities [--json|--markdown]Show the negotiated protocol versions, transports, capture backends and codecs this build actually speaks, plus its KDE Connect capability sets
linux-link kick <id|prefix|peer-ip|all>Drop a live streaming session (R4 D2)

Capabilities Are Generated, Not Written

linux-link capabilities is the single source of truth for the four numbers and lists a reader otherwise has to trust prose for: the negotiated protocol version, which transports a given build carries, which capture backends exist and in what order Auto tries them, and which codecs can be encoded/decoded on each side. Every value is read from the constants and decision tables the wire uses — protocol::{HANDSHAKE_HELLO, ALPN_V1_STREAM}, protocol::v2::{ALPN_V2, V2_MIN_VERSION..}, streaming::capture::capture_attempts, VideoCodec::ALL, AudioConfig::default() — so a client-profile build reports capture: unavailable instead of an empty list, and a version bump appears in the report the moment it appears in the code.

linux-link capabilities              # human listing
linux-link capabilities --json       # for scripts
linux-link capabilities --markdown   # the source of docs/capabilities.md

docs/capabilities.md is that Markdown output, committed, and server/tests/capabilities_doc.rs fails when it stops matching the build — regenerate it in the same commit as the constant you changed.

The Encoder Benchmark Is Pinned, Not Improvised

linux-link bench exists because "it felt laggy today" is not a regression test. It encodes one generated 720p clip — 300 frames, 5 Mbit/s, veryfast, H.264 — and reports the per-frame encode distribution (p50/p90/p95/p99/max). The clip is a pure function of its frame index (scene_frame), so the workload is byte-identical between runs without committing a gigabyte of raw frames, and it mixes a static gradient with a moving block so the encoder cannot coast on zero-residual skip blocks and measure nothing.

linux-link bench --target vaapi --repeat 5           # measure this machine
linux-link bench --baseline bench/baselines          # grade against a committed record
linux-link bench --record bench/baselines            # add this machine's record

A baseline is only comparable to a run on the same host, backend, geometry, bitrate, preset and codec — anything else is reported as SKIP (exit 3), never as a pass, because encode throughput does not travel between CPUs. --repeat N keeps the fastest run: other work on the machine can only make an encode slower, and on a desktop with a load average near 6 the best of seven runs reproduced the quiet-machine number (p50 7 ms) while a single run reported 11 ms. Encode time is the half this measures; decode and render live on the phone. CI runs the same command on every push — see bench/README.md.

Man Page

man linux-link

Android Client

The Android client is a native Kotlin app in android/, bridged to the shared Rust core via JNI/UniFFI.

cd android
./gradlew assembleDebug   # Debug build
./gradlew assembleRelease # Release build

Requires JDK 17+ and Android SDK/NDK.

Architecture

┌─────────────────────────┐         Tailscale          ┌─────────────────────────┐
│   Android Client        │◄──── Encrypted P2P ────────│   Linux Server          │
│   (Kotlin + Rust FFI)   │        (Tailscale IP)      │   (Hyprland)            │
├─────────────────────────┤                            ├─────────────────────────┤
│  UI Layer (Compose)     │                            │  Rust Daemon (tokio)    │
│  ├── Connection Screen  │                            │  ├── Screen Capture     │
│  ├── Remote Desktop     │                            │  ├── FFmpeg H.264 Enc.  │
│  ├── File Browser       │                            │  ├── QUIC Transport     │
│  └── Settings           │                            │  └── Adaptive Bitrate   │
├─────────────────────────┤                            ├─────────────────────────┤
│  Rust Backend (FFI)     │                            │  KDE Connect Plugins    │
│  ├── Connection Mgr     │                            │  ├── Battery            │
│  ├── Video Decoder      │                            │  ├── Clipboard          │
│  ├── File Transfer      │                            │  ├── Notification       │
│  └── Input Handler      │                            │  ├── Share              │
└─────────────────────────┘                            │  ├── File Browse        │
                                                       │  └── Input              │
                                                       └─────────────────────────┘

Data Flow (Streaming):

PipeWire → BGRA Frame → FFmpeg H.264 → QUIC Datagram → MediaCodec → SurfaceView

Data Flow (Input):

Touch Gesture → Rust FFI → QUIC/TCP → KDE mousepad packet → enigo → Wayland

Project Structure

Linux-Link/
├── core/                   # Shared Rust library
│   └── src/
│       ├── protocol/       # KDE Connect protocol + connection handling
│       ├── streaming/      # Capture, encoder, QUIC transport, adaptive bitrate
│       └── tailscale/      # Tailscale integration
├── server/                 # Linux server daemon
│   └── src/
│       ├── plugins/        # KDE plugins (5 + file browse)
│       ├── cli.rs          # CLI argument parsing
│       ├── config.rs       # TOML configuration
│       ├── kde.rs          # Plugin registry + service setup
│       └── service.rs      # Main server loop
├── android/                # Android client (native Kotlin)
│   ├── app/                # Kotlin app module (UI, services, MediaCodec decode)
│   └── bridge/             # Rust cdylib crate (UniFFI/JNI bindings to core)
├── aur/                    # AUR packaging (PKGBUILD)
├── docs/                   # Design specs and plans
├── man/                    # Man pages
├── scripts/                # Install script
├── .github/workflows/      # CI/CD (build, test, release, audit)
├── CHANGELOG.md            # Project changelog
├── CONTRIBUTING.md         # Contributor guidelines
├── config.toml.example     # Example configuration
├── linux-link.service      # systemd service file
└── plan.md                 # Full development plan

Quality Gates

CheckStatus
cargo fmt✅ Pass
cargo clippy -D warnings✅ Pass (0 warnings)
cargo test --workspace✅ 52 tests pass
cargo check --workspace✅ Clean compilation

Contributing

Linux Link is actively developed and looking for contributors!

Prerequisites

  • Rust 1.80+ (edition 2024)
  • JDK 17+ and Android SDK/NDK (for the Kotlin client)
  • Tailscale (for testing)
  • FFmpeg (runtime and dev libraries — the server encodes in-process via ffmpeg-next), PipeWire, xdg-desktop-portal (for streaming)

Development Workflow

# Build + test
cargo build --workspace && cargo test --workspace

# Format + lint
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings

# Android build
cd android && ./gradlew assembleDebug

See CONTRIBUTING.md for full guidelines.

Roadmap

All 6 development phases and the RustDesk research round (R4) have landed in code: the native Kotlin client, the Rust↔JNI bridge, MediaCodec decode, and the streaming/transport/security stack are in main, and v0.1.0 is tagged and pushed.

What is not done is verification and hardening, so treat this as beta rather than a release:

  • cargo fmt --all -- --check, clippy -D warnings across every core feature profile and cargo test --workspace all pass from a clean checkout of main (verified 2026-09-24). CI now runs that matrix plus the Android build, and cargo audit can fail a run — but no workflow has executed yet, because the branch has to be pushed first.
  • On-device verification for the large majority of shipped Android features (see docs/device-verification-checklist.md)
  • Cross-network (cellular ↔ LAN) WAN test over iroh; the Tailscale path is device-verified
  • A GitHub Release with artifacts (v0.1.0 is a tag only)

The full backlog, with every item's verified status against this tree, is docs/roadmap-3000.md; how that backlog gets executed is docs/roadmap-execution-plan.md. See plan.md for the development journal.


Stars, forks, and PRs are welcome! If Linux Link sounds interesting to you, drop a ⭐ and say hi.

Languages

Rust

75.8%

Kotlin

18.3%

HTML

3.0%

Shell

2.9%