ryll --rdp operator guide¶
ryll --rdp serves a SPICE session to a stock RDP client, so a desktop that
only speaks SPICE can be reached from a machine with an RDP client and no
SPICE one. One client is served at a time.
It is a reachability fallback, not a better path. The session is transcoded
from SPICE to RDP on the machine running ryll, which costs CPU there, and
the RDP client cannot use SPICE features such as USB redirection and folder
sharing. If you can run a SPICE client, run one. If all you have is a
browser, use --web.
Getting a binary¶
No release includes --rdp yet. The mode lives behind the rdp cargo
feature, which is off by default because it pulls in IronRDP and its
CredSSP stack, and the release builds leave it out. A binary built without
it refuses the flag:
Error: this ryll binary was built without the `rdp` feature
and exits with status 1. Today you build it yourself.
With Docker, from a checkout of the repository:
make release-rdp
This builds inside the project's devcontainer, as make release does, and
copies the binary to target/rdp-smoke/ryll. Use that copy rather than
target/release/ryll: both targets share target/release, so a later
make release replaces it with a build that has no --rdp.
With a host toolchain:
cargo build --release -p ryll --features rdp
which leaves the binary at target/release/ryll. The
development guide lists the build dependencies, and
installation points to it for building from source.
Quick start¶
ryll --rdp --file connection.vv
or, with no .vv file,
ryll --rdp --direct 192.168.1.100:5900
ryll connects to the SPICE server, starts the RDP listener and prints what to type into the RDP client:
RDP: 127.0.0.1:3389
user: ryll
password: <24 random letters and digits>
security: tls
certificate SHA-256: 3A:7F:...:C2
The fingerprint is 32 colon-separated byte pairs, in the form FreeRDP and
mstsc show when they ask whether to trust a certificate. It is printed only
when ryll generated the certificate; with --rdp-tls-cert it is your own.
The password is printed even when you chose it with --rdp-password. These
lines go to standard output, as do ryll's log lines.
Optional flags:
--rdp-host 127.0.0.1— bind address. Defaults to loopback; use0.0.0.0for LAN access. The address printed is the one bound, so with0.0.0.0it is0.0.0.0:3389, not something to connect to: use the host's own name or address.--rdp-port— TCP port. If you give none, ryll tries 3389 and uses an ephemeral port if 3389 is taken; the port bound is the one printed. A port you name is used or the launch fails, and0asks for an ephemeral port.--rdp-userand--rdp-password— what clients log in with. The user defaults toryll. The password defaults to 24 random letters and digits, generated at each launch.--rdp-security tls|nla— how clients authenticate. See Credentials and security modes.--rdp-tls-certand--rdp-tls-key— your own certificate instead of a self-signed one generated at launch. See Certificates.
--rdp cannot be combined with --web or --headless. --control-socket
works with it, and is unaffected by SPICE reconnects; see the
control socket protocol.
Every flag is tabulated in configuration.md.
Clients¶
Only one client has been tested.
| Client | Status |
|---|---|
FreeRDP 3.15.0 (xfreerdp3) on Linux |
Tested: display, keyboard, mouse, cursor, resize, clipboard, audio, both security modes, takeover and SPICE reconnect |
| mstsc on Windows | Expected to work; not tested |
| Windows App / Microsoft Remote Desktop on macOS, iOS and Android | Expected to work; not tested |
| Remmina and other FreeRDP-based clients | Expected to work; not tested |
"Expected to work" means the client speaks standard RDP, which is all ryll
asks of it. Nobody has checked. Differences between clients show up in
details such as which mouse buttons, audio formats and resize requests they
send, and the sections below say where they matter. A client that insists on
Network Level Authentication needs --rdp-security nla.
The FreeRDP command lines that matter:
xfreerdp3 /v:HOST:PORT /u:ryll /p:PASSWORD /cert:tofu /dynamic-resolution /sound
/cert:tofuremembers the certificate the first time and complains if it changes. With a generated certificate that is every launch; compare the fingerprint ryll printed with the one FreeRDP shows before accepting it./dynamic-resolutionlets the desktop follow the window when it is resized. Without it the client only sends the size it connected with./soundturns audio on./sound:format:1forces uncompressed PCM, the fallback ryll offers after Opus./sec:nlais for a server started with--rdp-security nla.-clipboardturns the clipboard off.
Other clients have their own settings for the same things; mstsc's are on its "Local Resources" tab.
What works¶
- Display: the guest's primary display. Only one monitor is served; if a client sends a layout with several, ryll uses the primary one and ignores the others. A client that is slow to take frames is sent the pixels as they are by the time it asks, never a backlog.
- Keyboard: key presses and releases as scancodes, and the client's Scroll, Num and Caps Lock state.
- Mouse: position, left and right buttons and the vertical wheel, in both SPICE mouse modes. When the guest runs vdagent, positions are absolute. When it does not, ryll turns the client's positions into relative moves, and the guest's own pointer acceleration makes the two pointers drift apart; moving the pointer back to the middle of the window and carrying on is the workaround.
- Cursor: the guest's cursor shape, sent as an RDP pointer (scaled down to fit 96x96 when larger), hidden and shown with the guest's. The guest's cursor position is not sent: the client draws the pointer where its own mouse is.
- Resolution: ryll asks the guest to use the RDP client's desktop size, both the size the client connects with and the size its window changes to. The size asked for is rounded down to a multiple of 8, so a window 1366 pixels wide gets a 1360 pixel guest. The guest only adopts a size if it runs vdagent. A connection whose size differs from the guest's sees a blank desktop for up to about 2 seconds at connect, before the client is resized to the guest's size.
- Clipboard: text, both ways, between the client and the guest (the guest needs vdagent). Line endings are converted between the client's CRLF and the guest's LF. A client's clipboard reaches the guest when it connects. Text the guest copied before the client connected is not offered to it; text copied afterwards is. Text the client copies that is longer than 100 MiB, the most ryll can send a guest's vdagent in one message, is not shared, and a warning is logged. Rich formats and files are not shared.
- Audio: the guest's audio plays on the client. ryll offers Opus and then 16-bit 48 kHz stereo PCM, and uses the first one the client accepts. Opus is SPICE's own packets passed through, a few tens of kbit/s. PCM is about 1.5 Mbit/s. ryll has no flag to turn either off; do it in the client. ryll never sends a volume change.
What is lost¶
- USB redirection and folder sharing from the client: the RDP client
cannot pass its own USB devices or folders to the guest. ryll's own
--usb-disk,--usb-disk-ro,--share-dirand--share-dir-roare passed to the SPICE session as in the other modes, giving the guest a disk image or a directory from the machine running ryll; that has not been tested through--rdp. - Unicode key events: some clients, mobile ones in particular, send a
character with no scancode. SPICE input is scancodes only, so ryll drops
them and logs a warning once:
RDP input: the client sent a Unicode key event, which SPICE cannot carry; dropping it. Characters that arrive as ordinary key presses are unaffected. - Side mouse buttons and horizontal scrolling: SPICE has neither, so they are dropped with a warning logged once.
- The middle button, with clients that send it on the standard mouse message: the RDP library ryll uses turns it into a plain pointer move, so the guest never sees the click. FreeRDP sends it on a channel of its own and is unaffected; this is issue 604.
- Audio the client did not accept: if the SPICE server sends raw PCM and the client chose Opus, ryll does not encode it and plays nothing; a warning is logged once.
Credentials and security modes¶
The username and password are compared in constant time and are case sensitive. The domain a client sends is ignored, because clients fill it in inconsistently. ryll does not count or limit failed attempts, so on a network you do not trust, leave the generated password in place: it has about 143 bits.
A generated password is printed at each launch, so it changes every time.
A password given with --rdp-password is visible to the other users of the
host in the process list.
--rdp-security chooses how clients authenticate, and a listener offers one
mode, not both:
tls(default): TLS, with the username and password sent inside it and checked by ryll. Every client speaks it, including clients that also offer NLA.nla: Network Level Authentication (CredSSP). The client proves it knows the password before the session starts. A client that cannot do NLA is refused.
Both run inside TLS, so both need a certificate.
Certificates¶
By default ryll generates a self-signed certificate at launch. It is held in
memory, so every launch has a new fingerprint, and clients that remember
certificates will complain at the next launch. The certificate names the bind
address and localhost; with 0.0.0.0 or :: that is localhost alone, so
a client connecting by another name sees a name mismatch.
To present a certificate clients can trust, or one that survives a restart, give both flags together (clap rejects one without the other):
ryll --rdp --file connection.vv \
--rdp-tls-cert /etc/ryll/tls/cert.pem \
--rdp-tls-key /etc/ryll/tls/key.pem
The certificate file is PEM and may hold a chain, leaf first. The key file may
hold a PKCS#1, PKCS#8 or SEC1 key if its name ends in .pem; with any other
name it must hold a PKCS#8 key. A file that cannot be read, a key that does
not match the certificate, or a missing flag fails the launch, before ryll
connects to SPICE. The certificate is read once at startup: to rotate it,
restart ryll.
The certificate recipes in the web guide work here too. A certificate from a CA the client trusts, for a name the client connects to, is the way to stop its warning.
One client at a time, and reconnect¶
A client that authenticates takes the session over from the one connected, which is disconnected, and any keys or buttons it held are released in the guest. A client that fails to authenticate leaves the connected one alone. Only one client at a time may be between connecting and authenticating; others wait unaccepted, and a client that has not authenticated within 30 seconds of being accepted is dropped.
If the SPICE connection is lost after a session has been established, ryll keeps the RDP listener and the connected client up, shows the client the last frame at half brightness, and retries SPICE after 1, 4 and 16 seconds. Keyboard and mouse input and resize requests made meanwhile are discarded, not replayed. When SPICE is back the client is sent the whole desktop at full brightness. Text on the clipboard is kept and reaches the guest once it is back.
ryll exits, disconnecting the client, when the first SPICE attempt never
reaches a session, when the three retries are used up, when the .vv file's
ticket is single-use or has expired so there can be no retry, and on
Ctrl+C. The exit status:
| Ended by | Status |
|---|---|
| Ctrl+C (SIGINT) | 0 |
| The first SPICE attempt never reached a session | 1, with that attempt's error |
| The retries used up | 1 |
| The ticket is single-use or expired when the session is lost | 1 if the lost session ended with an error, otherwise 0 |
session-lifecycle.md has the details, including what each of these looks like in the log.
What it costs¶
These numbers are from one run, to show the order of magnitude. They were
measured on 2026-10-21 on a release build with the rdp feature, using
tools/rdp-measure.sh against the ryll process: CPU (100% is one core,
and includes SPICE decoding and RDP encoding) and bytes sent to the client,
over 30 seconds each. The client was FreeRDP 3.15.0, which negotiated
RemoteFX, on the same 16-core host as ryll, showing a 1024x768 desktop
from a 2 vCPU QEMU guest.
| Workload | Average CPU | Peak CPU | Average rate |
|---|---|---|---|
| Idle (20 s) | 0.3% | 1.0% | 0.6 kbit/s |
| Typing in a terminal | 8.5% | 12.8% | 294 kbit/s |
| Scrolling a Wikipedia article | 84.6% | 131.1% | 1.12 Mbit/s |
| Video (480p VP9, 853x480 region) | 248.2% | 298.8% | 109 Mbit/s |
An idle or typing session is cheap. Scrolling costs most of a core. Video is
expensive because the client is sent bitmaps of a region that changes about
28 times a second: expect to need a core or more per client, and a fast link,
for anything like it. The client shared a host with ryll, so the numbers say
nothing about a real network. development.md, under
"Verifying --rdp", says how to repeat the measurement.
Service mode¶
For a long-lived deployment, run ryll under systemd. There is no reference
unit file in the repository; this one is adapted from
examples/ryll-web.service
and is a starting point, not something the project tests:
[Unit]
Description=ryll --rdp SPICE-to-RDP server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=ryll
Group=ryll
EnvironmentFile=/etc/ryll/rdp.env
ExecStart=/usr/local/bin/ryll --rdp \
--file ${VV_FILE} \
--rdp-host ${RDP_HOST} \
--rdp-port ${RDP_PORT} \
--rdp-user ${RDP_USER} \
--rdp-password ${RDP_PASSWORD} \
--rdp-tls-cert ${RDP_TLS_CERT} \
--rdp-tls-key ${RDP_TLS_KEY}
Restart=on-failure
RestartSec=5s
KillSignal=SIGINT
TimeoutStopSec=10s
StandardOutput=journal
StandardError=journal
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadOnlyPaths=/etc/ryll
Things a unit has to get right:
KillSignal=SIGINT. SIGINT is the clean stop: ryll disconnects the client, closes the listener and exits 0. ryll does not handle SIGTERM, so systemd's defaultKillSignalends the process at once, with no goodbye to the client.- A fixed password. A generated one changes at every restart, so the
service wants
--rdp-passwordfrom anEnvironmentFile. Create itroot:ryll, mode0640, and keep the password out of the unit file. It is still on ryll's command line, so other users of the host can read it from the process list; there is no password-file flag yet (issue 648). - The journal holds the password. ryll prints the launch lines, password
included, to standard output, and its log lines go there too. With
StandardOutput=journal, as above, anyone who can read the unit's journal can read the password. There is no flag to keep it out. - A fixed port. Without
--rdp-port, ryll silently moves to an ephemeral port when 3389 is taken. Name the port and a clash fails the start instead. -
Certificate files readable by the
rylluser, and the key not by anyone else:chown root:ryll /etc/ryll/tls/cert.pem /etc/ryll/tls/key.pem chmod 0640 /etc/ryll/tls/cert.pem chmod 0640 /etc/ryll/tls/key.pem
Rotate by replacing the files and restarting the unit.
- A reusable ticket. If the .vv file's ticket is single-use, ryll cannot
reconnect to SPICE and exits when the session is lost. If the session ended
with an error, Restart=on-failure then starts it against a ticket that has
been spent; if it ended cleanly, ryll exits 0 and is not restarted at all.
Either way the service stops serving. A service wants a .vv file that
stays valid, or --direct with --password-file.
- Reaching the client. --rdp-host 0.0.0.0 (or the interface address)
and a firewall rule for the TCP port. RDP is TCP only here.
Troubleshooting¶
this ryll binary was built without the rdp feature¶
The binary has no --rdp. See Getting a binary.
The launch fails naming the port¶
starting the RDP server: binding 127.0.0.1:3389: ...
You named a port with --rdp-port and something else is listening on it.
Pick another, or give 0 for any free one. Without --rdp-port this cannot
happen on 3389: ryll moves to an ephemeral port and prints it.
The launch fails naming the certificate¶
loading the RDP TLS identity from "cert.pem" and "key.pem": ...
The files could not be read or parsed. Check the permissions of the user ryll
runs as, and that they are PEM. the RDP TLS certificate and key are unusable
or do not match means the key is not the certificate's.
The client connects and is refused¶
ryll logs a warning, with the username but never the password:
RDP client presented wrong credentials
Check the username, the password (case sensitive) and, if the client insists
on NLA, whether the server was started with --rdp-security nla.
The client hangs at connect, then drops¶
Only one client at a time may be between connecting and authenticating. If
another client, or a port scanner, connected first and has not authenticated,
yours waits unaccepted until it does or is dropped after 30 seconds. ryll
logs the client did not authenticate within 30s for the one dropped.
A blank desktop for two seconds at connect¶
Expected when the client's size differs from the guest's: ryll asks the guest for the client's size, and if the guest does not adopt it the client is resized to the guest's after about 2 seconds. A guest with no vdagent never adopts it.
The desktop does not follow the window¶
The guest has to run vdagent, and FreeRDP needs /dynamic-resolution.
Sizes asked for are rounded down to a multiple of 8.
The pointer lands in the wrong place or drifts¶
The guest has no vdagent, so SPICE is in relative mouse mode; see
What works. Installing spice-vdagent in the guest avoids the
whole class of problem.
The middle button does nothing¶
See What is lost. Use FreeRDP, or wait for the fix to issue 604.
The client shows a darkened frame¶
The SPICE connection was lost, and ryll is retrying. The log says so:
rdp: reconnecting to SPICE in 4s (retry 2 of 3)
If the retries fail, ryll exits and logs why: rdp: not reconnecting to SPICE:
....
No audio¶
The client has to ask for it (/sound in FreeRDP). If ryll logs the SPICE
server sends raw PCM but the client chose Opus, which ryll does not encode;
not played, the SPICE server is sending raw PCM; force the client to PCM
(/sound:format:1).
Pasting long text does nothing¶
Text over 100 MiB is dropped, with the client's text is too long for the
guest; dropped in the log. Both ends need to be running their clipboard
channels: the guest needs vdagent.
Certificate warning¶
Expected with the generated certificate; see Certificates.
Security note¶
The default bind address is loopback, so a fresh ryll --rdp is reachable
only from the machine it runs on. Everything below is about changing that.
Binding 0.0.0.0 or a LAN address exposes a session that can type into the
guest to anything that can reach the port. The password is the only thing
standing in front of it. Use the generated one if you can, or a long random
one; use your own certificate and have clients check it, because a client
that accepts any certificate gives the password to whoever answers; and keep
the port off networks you do not control.
ryll does not count or limit failed logins. It does bound what an unauthenticated client can do: one at a time is accepted until it authenticates, and each is dropped after 30 seconds. That means a client that connects and says nothing can keep every other client waiting for 30 seconds at a time, repeatedly. The listener has no other defence, so firewall it if that matters.
Both security modes run inside TLS. A client that does not check the
certificate cannot tell ryll from someone in between, and in tls mode that
someone is handed the password. Verify the fingerprint either way.
CI smoke lane¶
A nightly lane drives FreeRDP against ryll --rdp against a QEMU guest and
checks authentication, the frame, input, a client reconnect, takeover, a
SPICE reconnect and Ctrl+C. It is advisory, not a gate on pull requests; see
the RDP smoke lane. Run it locally with
make rdp-smoke.