Xpra client implemented in rust, for MS Windows, Linux and macOS.
It builds on MS Windows, Linux (X11 and Wayland) and macOS.
It supports tcp/ssl/ws/wss connections, plus ssh (via a subprocess, see below) and direct Unix-domain
socket connections on Unix platforms. ssl/wss verify the server's certificate against the system trust
store, unless --ssl-insecure says otherwise. Password authentication is supported (HMAC digest challenges —
see Authentication below).
It requires an xpra 6.6 or later server: every packet it sends uses the packet types introduced in
xpra 6.5, the window-ack draw acknowledgement and the clipboard-data layout only settled in 6.6.
The hello says so — protocol-version: [6, 6] is the oldest peer this client will talk to, and a server that
cannot satisfy it says "incompatible version" instead of failing later on an unknown packet type.
The server may be run in either mode: nothing sent depends on XPRA_BACKWARDS_COMPATIBLE, and the
packets the client receives are accepted under both their pre-6.5 and their current names.
There is no server implementation. Plain-text clipboard synchronization is supported, as is automatic server-to-client speaker forwarding on Windows. On Linux and macOS, a server running on the same host sends its pixels through shared memory rather than the socket — see Shared memory transfers.
On MS Windows there is a system tray icon with an Exit menu entry, and server-forwarded notifications are shown as balloons on it — see System tray. Elsewhere notifications are only written to the client log.
Ctrl-C ends the session gracefully rather than killing the client under a live connection: the server is told
why we are leaving (it logs client has requested disconnection: client interrupted) before the process exits
with code 0. SIGTERM and SIGHUP do the same on Unix, as does closing the console window on MS Windows. A
second interrupt terminates the process the ordinary way, so a shutdown stuck on a dead connection is still
interruptible. On Linux and macOS, where there is no tray icon and therefore no Exit menu entry, this is the only
clean way out.
On Windows 10 and later, server audio is enabled automatically when the system Media Foundation Opus decoder and
the default WASAPI output endpoint are available. The client negotiates only the bare opus codec (no Matroska
or Ogg container), receives audio asynchronously, and renders it through a bounded adaptive jitter buffer. If
the native probe or output-device recovery fails, audio is disabled for that session without disconnecting it.
Speaker forwarding is receive-only in this milestone: microphone forwarding, non-Opus codecs, and audio output on Linux/macOS are not implemented.
The windowing/painting layer is built on winit + softbuffer, which run on both platforms, but the Wayland protocol itself does not let clients query or set their absolute desktop position:
- Override-redirect windows (used by the server for tooltips/menus/dropdowns) have no Wayland equivalent at all, and degrade to undecorated, non-resizable but still WM-managed windows (visible in taskbars/window-switchers, unlike true override-redirect).
- Server-initiated window moves (
window-move-resize) only apply the size on Wayland; the position component is silently skipped. - Outgoing window geometry (
window-map/window-configure) reports(0, 0)as the position on Wayland, since there is no OS API to query it. - NumLock state is not reported to the server (winit does not expose toggle/lock key state, only held modifiers).
Running under XWayland (the X11 backend) instead of native Wayland avoids all of the above.
Server-forwarded bells (bell) play a real tone on Windows, but on Linux fall back to writing the terminal
bell (^G) to stderr - there is no portable desktop bell without an X11 or audio-server dependency, which this
client avoids - so a bell is only audible when the client was started from a terminal whose bell is enabled.
There is no system tray icon on Linux either (see System tray below): the freedesktop StatusNotifierItem protocol needs a D-Bus dependency, and the older XEmbed tray is X11-only. Server-forwarded notifications are shown as balloons on that tray icon, so they too are Windows-only and are merely logged here.
Everything that is not written against a platform API is the same code as on Linux: the connections, the
clipboard, shared memory transfers and the Ctrl-C shutdown above. What is
missing is what is implemented against Windows:
- no system tray icon, and so no Exit menu entry (
Ctrl-Cis the way out) and no desktop notifications — as on Linux, the client only logs them; - no
h264: that decoder is the operating system's, through Media Foundation, so the server picks amongjpeg/png/webp— or shared memory, when it runs on the same host; - no speaker forwarding;
- a server-forwarded bell writes the terminal
^Grather than playing a tone, as on Linux.
Override-redirect windows (tooltips, menus, dropdowns) have no macOS equivalent either: they are created undecorated and non-resizable, but remain ordinary managed windows, as on Wayland.
The published binary is Apple silicon (arm64) only; on an Intel Mac, build from source.
Pre-built binaries for MS Windows, Linux and macOS are attached to each release on GitHub.
Each release also carries a CycloneDX SBOM per binary (xpra-<platform>.cdx.json), generated by
cargo cyclonedx from the same build's Cargo.lock: the
crates it was built from, with their licenses (plus a few optional ones that are never compiled in, since
cargo cyclonedx does not resolve features). The macOS and arm64 Windows binaries are also built with
cargo auditable, so they embed that list too, readable by
cargo audit bin, syft, trivy and grype; the Linux and x86_64 Windows ones do not, since they are
UPX-compressed, which would hide that data from those tools. The SBOM lists Rust crates only: the C libraries
some of them compile in (libjpeg-turbo, libwebp, zlib) appear only as the -sys crates that vendor them.
Linux packages (RPM and DEB) are also published to the xpra repositories, under the package name
rust-xpra — see Download for how to add the repository for
your distribution, then:
dnf install rust-xpra # or: apt install rust-xpraThese packages are currently published to the beta repository. They install the client as
/usr/bin/rust-xpra rather than /usr/bin/xpra, so that they can be installed alongside the python xpra
package (which owns that path); everything below that calls the binary xpra refers to a locally built one.
cargo build
./target/debug/xpra # asks for the connection details, see below
./target/debug/xpra HOST:PORT
./target/debug/xpra tcp://HOST:PORT/
./target/debug/xpra ssl://HOST:PORT/
./target/debug/xpra ws://HOST:PORT/
./target/debug/xpra wss://HOST:PORT/
./target/debug/xpra ssh://[USER@]HOST[:PORT]/[DISPLAY]
./target/debug/xpra socket:///run/user/1000/xpra/10 # Unix only
./target/debug/xpra /run/user/1000/xpra/10 # equivalent shorthand
./target/debug/xpra --ssl-insecure ssl://HOST:PORT/ # skip certificate verification
./target/debug/xpra --remote-xpra=/opt/xpra/bin/xpra ssh://HOST/10 # remote xpra off PATH
./target/debug/xpra --help # or -h: the same list, plus the environment variables
./target/debug/xpra --version # this client's own version (not the xpra protocol version)Options and the target may be given in either order. An argument starting with - that is not a known option
is an error rather than something to connect to, so a mistyped option can never be read as a hostname.
Started without any argument, the client opens a small connection dialog instead of exiting: a protocol
drop-down (which pre-fills the port with that protocol's default — 10000, or 22 for ssh), a host, a port, and
an optional username and password, plus Cancel and Connect. Tab moves between the fields, the arrow
keys pick the protocol, Enter connects and Esc cancels (exit status 0). The connection is made in the
background, so the dialog stays responsive and reports a failure (wrong port, no server, bad certificate, ...)
in place, ready for another attempt, rather than exiting.
The username is only part of the connection itself for ssh (ssh://USER@HOST/); for the other protocols it is
sent in the client's hello, which is what a server authenticating per-user matches against. The password is
the session password — it answers the server's authentication challenge (see Authentication)
without prompting again — and never the ssh password: ssh asks for its own credentials on the terminal.
Like the password prompt, the dialog is drawn with the same winit/softbuffer stack as the rest of the client
(there is no widget toolkit here — windows are server-rendered pixels), with its text blitted from a bundled
bitmap font: Spleen 8x16, Copyright (c) 2018-2026, Frederic Cambus,
BSD-2-Clause.
Only the tcp, ssl, ws, wss, ssh and socket protocols are supported; any other protocol in the URI is
rejected. On Unix platforms, socket:///absolute/path connects directly to a filesystem pathname Unix-domain
socket; a bare /absolute/path is equivalent shorthand. This does no :DISPLAY lookup or socket-directory
discovery, and abstract-namespace sockets are not supported. Socket targets are command-line only and do not
appear in the no-argument connection dialog. They are unavailable on Windows.
ws support (HTTP upgrade handshake and frame framing) is hand-rolled against std only, to avoid pulling in a
websocket crate and its dependencies. ssl/wss use native-tls (OpenSSL on
Linux, Schannel on Windows, Security.framework on macOS) rather than a hand-rolled implementation, since TLS
encryption (unlike WebSocket masking) is a real security boundary. The server's certificate chain and its
hostname are verified against the system trust store; --ssl-insecure turns both checks off, for the
self-signed certificates xpra servers commonly use on local and test setups:
./target/debug/xpra --ssl-insecure ssl://HOST:PORT/A connection made with --ssl-insecure is encrypted but not authenticated, and so is open to interception —
it is no better than tcp:// against an active attacker. There is no way to trust a private CA yet, so that
flag is currently the only way to reach a server whose certificate the system does not already trust.
ssh shells out to the system ssh binary and uses its stdin/stdout pipes as the byte stream (no SSH library
dependency), running xpra _proxy [DISPLAY] on the remote end — the same mechanism xpra's own client uses to
bridge stdin/stdout to an existing display's socket. This requires a working ssh in PATH (OpenSSH on Linux and
macOS, or the bundled OpenSSH client on Windows 10 1809+) and xpra installed on the remote host.
--remote-xpra=PATH runs that xpra from a path instead of looking the name up on the remote login shell's
PATH, which is what reaches a relocatable install under a shared prefix — how xpra is deployed on hosts that
carry no xpra package and where no one has root. The path goes through the same command -v guard as the bare
name, so a wrong one is reported as no xpra command found: <path> rather than as a broken connection. The
option is only meaningful for ssh://, and giving it with another target is an error.
Authentication must not require interactive input on stdin (stdin carries the xpra protocol, not a terminal), so use key-based auth via an ssh-agent or a passphrase-less key; host-key confirmation and password prompts still work normally since OpenSSH reads those from the controlling terminal, not stdin.
On MS Windows the client puts an icon in the notification area for as long as it is connected. Right-clicking it
opens a menu naming the session (xpra @ HOST:PORT) with an Exit entry, which is the only way to shut the
client down from the GUI — closing a window only tells the server to close that window. Exiting this way sends a
disconnect to the server first and exits with status 0.
The icon is assets/xpra.ico, embedded into the executable by build.rs (which also embeds exe.manifest, the
per-monitor-V2 DPI manifest, so it is the executable's icon in Explorer too). It is implemented directly on
Shell_NotifyIconW through the windows crate that Media Foundation already requires, so it adds no new
dependency and no extra thread — the tray window is created on the UI thread and winit's own message loop pumps
it.
The same icon also carries desktop notifications: a notification forwarded by the server becomes a balloon
on the tray icon, with the notification's summary as the title and its body as the text. This needs no notifier
library — it is one more Shell_NotifyIconW call — which is why it is Windows-only. On Windows 10 and later the
shell renders these as toasts, so they follow the user's notification settings and Focus assist, and may land in
the Action Center instead of appearing on screen.
Only the text is used: notification actions (buttons) and hints are ignored, as are per-notification icons and the server's expiry timeout (Windows has ignored the requested balloon timeout since Vista, in favour of the system accessibility timeout). A notification the server withdraws is taken back, if it is still the one being shown.
There is no tray, and therefore no notifications, on Linux or macOS — the client just logs them (both platforms would need a D-Bus dependency this client avoids).
Servers that require a password (xpra's password/file/multifile/sqlite/pam/... auth modules) send a
challenge; the client answers it with an HMAC-SHA256 digest of the password. When a challenge arrives, the
password is obtained from — in order:
- the connection dialog's password field, when the client was started without arguments and it was filled in;
- the
XPRA_PASSWORDenvironment variable, if set (non-interactive; handy for scripts); pinentry, if one is found onPATH(honouringPINENTRY_PROGRAM) — the same native, secure prompt GnuPG uses (GTK/Qt/curses on Linux,pinentry-macon macOS);- otherwise a small built-in password dialog (drawn with the same
winit/softbufferstack as the rest of the client), so a prompt is always available — including on Windows, wherepinentryis normally absent.
Only the hmac+sha256 digest is implemented (it is the only one advertised, so the server always picks it);
Kerberos/GSS/SCRAM/U2F and the legacy xor/des digests are not supported and fail cleanly. The HMAC response
never reveals the password itself, and the server mixes in a fresh per-connection salt so a captured response
cannot be replayed — but the session payload is still in the clear over tcp/ws, so use ssl/wss (or ssh)
for confidentiality.
jpeg (libjpeg-turbo), png (libspng), webp (libwebp), and h264 on Windows only (decoded by the OS through
Media Foundation, no codec is bundled).
When the server runs on the same host as the client, encoding pixels only to decode them again is wasted work.
Xpra's answer is mmap: the client creates a backing file, maps it shared and tells the server where it is, and
the server writes raw uncompressed frames straight into it — the draw packets then carry nothing but offsets
into that area. Transfers become lossless and cost a memcpy instead of a decode.
This is on by default on Linux and macOS (as it is in xpra's own client), for the server → client direction only. Nothing has to be configured and nothing is lost when it does not apply: the client always offers an area, and a server that cannot open the file — because it is on another host — simply declines, leaving the session on the usual picture encodings. Neither side compares hostnames; each writes a random token into the area for the other to read back, which is what establishes that the two really are looking at the same memory.
The backing file is created in the temporary directory, is 128MB and sparse, and is unlinked as soon as the server has it open, so it never outlives the handshake. Note that while mmap is in use the server stops using every other picture encoding, which is the intended effect.
| Variable | Effect |
|---|---|
XPRA_MMAP |
no to switch it off; an absolute path to pin the backing file (an existing file is used as-is and never removed, which is how a virtio-shmem device is shared between a host and a guest) |
XPRA_MMAP_DIR |
the directory to create the file in (the temporary directory by default) |
XPRA_MMAP_SIZE |
the size of the area, with an optional K/M/G suffix (128M default, 64M minimum — the server rejects anything smaller) |
Not implemented: the client → server direction (the server only uses it for webcam frames, which this client does not have), and Windows, where the equivalent is a named file mapping rather than a file and would only ever help against a shadow server on the same machine.
On a display scaled above 100% (a Retina Mac, a 4K laptop panel at 200%) the session runs in logical pixels: the client reports its desktop and monitors divided by the display's scale factor, and draws every window scaled up by that factor. Remote applications therefore come out at the size they were designed for, which is what xpra's own client does through GTK. The factor is the primary monitor's and holds for the whole session; scaling is nearest-neighbour, so at an integer factor every server pixel becomes a crisp block.
| Variable | Effect |
|---|---|
XPRA_DESKTOP_SCALING |
off for one server pixel per screen pixel (tiny windows on HiDPI), or a factor such as 1.5 / 150% to force one; follows the display by default |
By default libwebp is built from the vendored C sources and linked statically, so that the release binaries
are self-contained — this needs no extra tooling (a C compiler only: no cmake, no nasm, no bindgen).
Distribution packages generally must not bundle their own copy of a library the distribution already ships and has
to be able to patch, so there is a feature to link the system libwebp shared library instead (located with
pkg-config):
cargo build --release --features webp-dylibKeeping the dependency graph small is a deliberate constraint here — it is why the WebSocket layer and the SHA1
and HMAC-SHA256 implementations are hand-rolled against std rather than pulled from crates, and why ssh
shells out to the system binary instead of linking a client.
xpra-org.github.io/rust-xpra maps what is
actually there: for each direct dependency, its full transitive closure, which crates it is the only route to
(what dropping it would really remove), and which of those are linked into the binary rather than merely run at
build time. The same page is committed as docs/dependency-graph.html and is entirely self-contained, so it
works offline from a checkout too.
As of 0.3.0, the Linux graph is 124 crates — 98 linked and 26 build-time only (build scripts and proc-macros) —
against 77 on Windows, of which 53 are linked and 24 build-time only. The windowing stack dominates both: winit
alone accounts for 76 of the Linux total, 33 of which nothing else reaches, and softbuffer adds only 9 more on
top of it. The page also flags crates present at more than one version — rustix and linux-raw-sys on Linux,
the windows-sys/windows-result/windows-strings trio on Windows.
Regenerate it after changing a dependency (needs python 3 and cargo, nothing else):
python3 docs/dependency-graph.pydocs/README.md describes what the page shows, how the data is derived, and how it is
published.
