Skip to content

irobinson010/rd-linux

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

rd-linux — current-session Wayland remote desktop (WebRTC + NVENC)

A self-hosted remote-desktop server for KDE Plasma / Wayland that lets you control your existing logged-in session from a browser. Unlike RDP/xrdp — which spin up a new session on Wayland — this attaches to the live one, so you see and drive exactly what's on the screen.

Video is captured via the xdg-desktop-portal ScreenCast API, encoded on your GPU (NVENC, software x264 fallback), and streamed to a browser over WebRTC. Mouse and keyboard travel back over a WebRTC data channel and are injected with a virtual uinput device. It works over a private overlay like Twingate with no public exposure.

Laptop/phone browser  ──(Twingate)──►  Your PC
  <video>/<audio> (WebRTC)              aiohttp  : serves the client + WebSocket signaling
  input capture (JS)                    GStreamer: pipewiresrc → NVENC → webrtcbin
        │                               portal   : live-session screen capture
        └─ input over datachannel ──────► uinput : virtual keyboard + absolute pointer

Highlights: multi-monitor with live screen switching, system audio, adaptive codec fallback (H.264 → VP8 so even Linux/Firefox clients work), browser-side resolution & bitrate, fullscreen-takes-control with keyboard shortcuts, pinch-zoom for phones, TLS, and an unattended mode you can start over SSH.


Requirements

  • KDE Plasma on Wayland (developed on Plasma 6 / Kubuntu). Needs xdg-desktop-portal-kde, PipeWire, WirePlumber — standard on Kubuntu.
  • NVIDIA GPU with NVENC recommended (software x264 works but is heavier).
  • A browser on the client (Chrome/Edge/Safari ideal; Firefox & Linux browsers work via the automatic VP8 fallback).
  • Optional: Twingate (or any VPN/overlay) for remote access without exposing ports.

Quick start

git clone https://github.com/irobinson010/rd-linux.git
cd rd-linux
./install.sh          # installs deps, generates a random token, sets up the service
./rd.sh start         # start it (approve the KDE share dialog once)

./rd.sh start prints your connect URL, e.g.:

connect: https://192.168.1.50:8098/?token=<your-random-token>

Open that in the client browser. Because TLS uses a self-signed cert, the browser warns once — accept it. Click Take control (or go Fullscreen, which takes control and captures keyboard shortcuts). Double-tap Esc to release control.

The access token is generated by install.sh and stored in ~/.config/rdserver/rd.env (mode 600) — it is not in the repo. Treat the URL like a password: anyone with it can control your machine.


Running it

rd.sh is the control script. It exports the session environment needed to reach the portal/PipeWire (so it also works over SSH) and drives the systemd --user service:

./rd.sh start      # start (default)            ./rd.sh status   # is it running
./rd.sh stop       # stop                       ./rd.sh log      # follow logs / show URL
./rd.sh install    # (re)install the unit after editing deploy/rdserver.service

Auto-start at login:

systemctl --user enable rdserver

Start it over SSH (the script handles the environment, so this just works):

ssh you@your-pc
cd rd-linux && ./rd.sh start

The service runs inside your user session manager, so it keeps running after the SSH session closes, and restarts itself on failure.


Unattended mode (--unattended, the default for the service)

Normally the screen-share portal that also grants input control can't be remembered, so you'd have to click "Allow" at the machine every start. --unattended avoids that:

  • Input goes through a virtual uinput device (no input portal, ever). Needs write access to /dev/uinput, which your graphical session already grants you via a systemd-logind ACL — no setup.
  • Capture uses a persistent ScreenCast grant: you approve the share dialog once, a restore token is saved to ~/.cache/rdserver/, and later starts (including over SSH) restore the capture silently.

Caveats: the machine must be logged in to the desktop (use auto-login for a fully headless boot), and a locked session may capture as black depending on your KDE version.


Connecting over Twingate

Twingate gives the client a private path to the PC — no public exposure or TURN needed. Define the PC as a Twingate Resource and allow:

  • TCP 8098 — the web client + signaling (change with --port).
  • UDP to the host for the WebRTC media (ephemeral high ports).

Use the PC's Twingate address (or LAN IP locally) in the URL. If the browser's WebRTC connectionState reaches failed, UDP isn't getting through — check that first.


Screen-sharing (view-only links)

Several people can connect at once: one controller + a few view-only viewers. A viewer gets the live video and audio but can't move the mouse or type — though they can still pick their own resolution/bitrate/screen/zoom.

Two ways to hand out view-only access:

  • From the control session, click "Share view" — it mints a view-only token and copies a ready-to-send link to your clipboard. Valid until the server restarts.
  • A permanent link: start with --view-token <token>; that URL always grants view-only access.

Each viewer is its own hardware encode, so keep the count modest (--max-viewers, default 4). Recipients must be able to reach the host (same Twingate/LAN), and the link carries a token — treat it like any other access link.

Options

Set these in ~/.config/rdserver/rd.env (RD_OPTS="...") for the service, or pass them to python3 -m rdserver directly.

flag meaning
--port N HTTP/signaling port (default 8089; the service uses 8098)
--token T fixed access token (default: random each start; install.sh pins one)
--view-token T second token for view-only access (watch, no control); also generatable in-app via "Share view"
--max-viewers N max simultaneous view-only sessions (each is its own encode; default 4)
--tls serve HTTPS/WSS. With no cert flags it auto-generates a self-signed cert
--tls-cert FILE use a real certificate (PEM, full chain) — see "Custom domain" below
--tls-key FILE matching private key (PEM); must be readable by the service user
--audio stream system audio (taps the default sink monitor, stereo 48 kHz)
--unattended uinput input + persistent capture (SSH-startable; see above)
--bitrate K initial video bitrate kbps (default 20000; also live in the toolbar)
--abr adaptive bitrate (WebRTC congestion control). Off by default — see note
--av1 force NVENC AV1 for all clients (needs install-av1.sh + a HW-AV1 client)
--udp-ports A-B RTP UDP port range (default 50000-50019)
--software force x264 software encoding instead of NVENC
--no-cursor don't embed the cursor in the video
-v verbose logging

--abr / audio note: adaptive bitrate is off by default because driving the encoder from the live bandwidth estimate churns the shared transport over tunneled links (Twingate) and makes audio choppy. Leave it off for steady audio; enable it only on variable real networks.


Custom domain & trusted certificate (no browser warning)

The default --tls uses a self-signed cert, so the browser warns once. To get rid of that, point a domain at the machine and feed in a real certificate with --tls-cert / --tls-key. Example for rd.labxp.net:

  1. DNS: make rd.labxp.net resolve to the PC (a Twingate-accessible address, or your internal DNS).
  2. Get a trusted cert. If the host isn't publicly reachable on port 80/443 (e.g. Twingate-only), use an ACME DNS-01 challenge — it proves ownership via a DNS TXT record, no inbound exposure:
    sudo certbot certonly --preferred-challenges dns -d rd.labxp.net
    # (or acme.sh / your DNS provider's plugin)
  3. Make the files readable by your user — the service runs as you, not root, and Let's Encrypt's privkey.pem is root-only by default. Copy them somewhere you own (and re-copy on renewal, e.g. via a certbot --deploy-hook):
    install -m 644 /etc/letsencrypt/live/rd.labxp.net/fullchain.pem ~/.config/rdserver/
    install -m 600 /etc/letsencrypt/live/rd.labxp.net/privkey.pem   ~/.config/rdserver/
  4. Point the server at them — edit ~/.config/rdserver/rd.env:
    RD_OPTS="--port 8098 --tls --tls-cert /home/you/.config/rdserver/fullchain.pem --tls-key /home/you/.config/rdserver/privkey.pem --audio --unattended --token <yours>"
    
    then ./rd.sh start. Connect via https://rd.labxp.net:8098/?token=... — no warning.

Certs renew ~every 90 days; restart the service after each renewal (a certbot deploy hook that copies the files and runs systemctl --user restart rdserver automates it).

Video codecs & browser compatibility

The server encodes H.264 (High or constrained-baseline), VP8, or AV1. You don't pick — the client auto-negotiates down a chain until frames actually decode, and remembers the winner per device (localStorage):

  1. high — H.264 High profile (best; hardware-decoded on phones, Windows, macOS).
  2. baseline — H.264 constrained-baseline, if High won't decode.
  3. vp8 — decodes in every browser in software; the universal fallback.

This matters because Firefox and Chromium/Brave on Linux with NVIDIA often have no usable H.264 decoder in WebRTC at all and would otherwise show black — they land on VP8 automatically. VP8 is software-encoded, so drop the resolution in the toolbar if it's heavy. To re-test a sharper codec on a device that fell back, clear that site's data.


Security notes

  • Access = the token (and your overlay/VPN). It's randomly generated at install, stored in ~/.config/rdserver/rd.env (mode 600), and never committed. Anyone with the URL can control the machine — don't share or paste it anywhere.
  • TLS (--tls) encrypts the signaling/token on the wire with a self-signed cert; the WebRTC media is always DTLS/SRTP-encrypted. Still, don't bind this to a public interface — keep it behind Twingate/VPN.
  • One controller at a time — a new connection replaces the previous one.
  • This grants full control of your desktop. The first capture approval is a real KDE security prompt; --unattended remembers only the capture grant (input is local uinput). Revoking the share in KDE forces re-approval.

Uninstall

./uninstall.sh

Removes the service and the generated config/cache (token + cert). It leaves apt packages, the optional AV1 plugin, and this source directory in place.


Optional: hardware AV1

./install-av1.sh

Builds the AV1 RTP payloader (rtpav1pay) from gst-plugins-rs (not packaged on Ubuntu) into ~/.local/lib/gstreamer-1.0. Then run with --av1. Only useful for clients that can decode AV1 in hardware — software AV1 decode is usually too slow (stutters).


Troubleshooting

  • Black screen in the browser → it's almost always codec decode; the client should auto-fall-back to VP8 within a few seconds. Check the page console for framesDecoded.
  • connectionState: failed → WebRTC UDP isn't reaching the host (Twingate/firewall).
  • Audio choppy → make sure --abr is off (default).
  • Service won't start / restart loop./rd.sh log. Over SSH, use ./rd.sh (it sets the env) rather than calling python3 or systemctl bare.
  • Re-prompted for the share dialog → the saved grant was revoked (KDE "stop sharing") or the session wasn't reachable; in --unattended it restores silently otherwise.

Layout

install.sh              install deps + generate token + set up the service
uninstall.sh            remove the service + generated config
install-av1.sh          optional: build the AV1 RTP payloader
rd.sh                   control script (start/stop/status/log/install), SSH-safe
deploy/rdserver.service systemd --user unit (token comes from ~/.config/rdserver/rd.env)
rdserver/
  __main__.py           entry point + CLI
  portal.py             ScreenCast / RemoteDesktop portal negotiation (+ persistence)
  media.py              GStreamer webrtcbin pipeline (capture → NVENC → WebRTC) + dispatch
  uinput_inject.py      virtual keyboard + absolute pointer (unattended input)
  signaling.py          aiohttp HTTP + WebSocket signaling, token auth
  keymap.py             browser event codes → evdev codes
  smoketest.py          standalone capture+encode validator
web/
  index.html / app.js / style.css   browser client
tests/                  offer/level regression tests + a manual uinput mouse test

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors