lamco-rdp-server 1.4.4: Calm on the surface, paddling like hell underneath

The real story behind lamco-rdp-server 1.4.4: 231 commits, a cross-vendor Vulkan encoder, and four months of work across GNOME, KDE, and COSMIC.

GL
Greg Lamberson
· 12 min read

There's an old line about a duck on a lake. From the bank, it looks serene: It just glides. Under the water, its feet are going like mad, and nobody watching ever sees that part. People have been saying some version of this for close to a century, about diplomats and actors and stressed-out students, and it fits software releases just as well.

From the outside, 1.4.4 looks like a small bump. One point release, up from 1.4.2, nothing that screams for attention. Underneath: Four months, 231 commits, and a server that now has to glide smoothly across something like nine different Wayland compositors, split across a handful of genuinely separate ecosystems that agree on almost nothing. GNOME does things its way. KDE does things its way. The wlroots family, Sway, Hyprland, River, Wayfire, labwc, does things its way. COSMIC, still finding its own way, does things yet another way. Getting one server to look calm across all of that took a lot of paddling. If you read the release announcement, you already have the headline version. Here's what was actually under the water. If you want the itemized version instead, the full changelog has every line. This is the story behind it.

The numbers underneath the calm surface

I went back through the actual git history for this one instead of just trusting the changelog, and the real shape of it surprised even me. 231 commits landed across 566 files. Strip out the internal analysis notes that never ship and a generated tool cache that isn't real content, and you're left with roughly 63,000 lines of actual shipped change: Source, docs, packaging, tests.

Here's the number that stuck with me: The src/ tree alone picked up 38,216 changed lines, and the whole current codebase's src/ is 94,092 lines. That's nearly two-fifths of the entire server, touched in one release window.

By that math, this could have plausibly called itself 1.5.0, maybe even 1.6.0. It didn't, on purpose. The way I use version numbers, they track what changes for the person on the other end of the connection, the protocol and API surface a client actually sees, not how much moved underneath. Nothing about how an RDP client talks to lamco-rdp-server broke or needed to change across this entire stretch, so it stayed a 1.4.x release no matter how much work went into it. I'm keeping 1.5.0 reserved for the multicodec work that's next, because that's the thing that will actually touch that surface. Staying disciplined about what a version number promises was part of the job here, right alongside everything else, and I'm genuinely proud of how much got done while the number barely moved.

What happened to 1.4.3

If you watch the tags, you might have noticed there's a gap. Here's the real story, because I think it's worth telling instead of just letting the number skip past quietly.

By May 21, this release was fully scoped as 1.4.3: New transports, the internals for a separate hypervisor-console product, a new Vulkan encoder, a metrics server. Three more weeks of testing turned up more fixes, and I rebased the IronRDP fork on top of all of it. Then, on June 9, I ran a full deployment check on real hardware and found problems that hadn't shown up anywhere else. The packaged systemd units were killing the server outright on startup: A seccomp filter was blocking the syscall PipeWire's realtime setup needs. PAM authentication was structurally broken under that same hardening too, in exactly the multi-user case where PAM is supposed to matter most. Both got root-caused and fixed.

By the time all of that landed, on top of an exhaustive testing campaign on dozens of configured VMs that found two more new bugs and confirmed nothing else had regressed, so much had piled up past the original 1.4.3 scope that I renamed the whole thing 1.4.4 and let 1.4.3 go. It never got a tag, a build, or a release anywhere. If you're wondering where it went: Nowhere, it just never happened, and 1.4.4 is what came out the other side.

New capability that showed up along the way

A few real additions, not just fixes:

A unified way of accepting connections now sits underneath every transport. On top of it: AF_VSOCK for Hyper-V Enhanced Session Mode, so Hyper-V guests get auto-detected and just work. WebSocket and RDCleanPath transport for browser and WASM clients. It's still experimental: I haven't exercised it end to end against the WASM client yet, so treat it as early. And LISTEN_FDS socket activation, so systemd can hand a single process file descriptors for several transports at once.

There's also a cross-vendor Vulkan Video encoder, using VK_KHR_video_encode_h264, that runs on NVIDIA, Intel, and AMD alike wherever the driver supports Vulkan Video. It sits next to VA-API and NVENC under one hardware-encoding umbrella, and the video encoding pipeline page on the site has the full breakdown of how the codec and hardware paths fit together. I don't know of another Linux RDP server that does this today.

And there's an HTTP metrics endpoint, Prometheus-style, plus a JSON health check, backed by a real session health sensor framework: PipeWire, the portal, Mutter, EGFX, and the active encoder all report in, and the GUI's status and performance tabs now show it live.

Nine compositors, four ecosystems, one server

This is the part of the release that actually explains the duck. Wayland gave every compositor the same rough shape, and then each one built its own rules on top of it, so "support Wayland" is really "support several unrelated things that happen to share a name."

GNOME got the biggest single fix in this release. It used to serve exactly one RDP session and then need a restart, because the server kept one long-lived session alive and Mutter's own idle timeout would reap it about eight seconds after a client disconnected, with no way to rebuild it. I replaced that with a session-lifecycle policy that gives Mutter a fresh session on every connection instead. That one change dragged several smaller races along with it: Capture nodes that could bind to a dead PipeWire stream after a reused node id, EGFX state that needed resetting per connection, clipboard listeners that needed to re-subscribe. Separately, GNOME 49 broke input entirely until the EIS lifecycle handling caught up with it.

KDE had a clipboard bug where copying a file from Windows to Linux silently failed. Klipper was re-announcing a file as plain text, and the handler meant to cooperate with it was forwarding that re-announcement instead of respecting the portal's own decision to block it. Underneath that, the clipboard monitor was watching the wrong protocol the whole time. KWin speaks ext-data-control-v1, not wlr-data-control-v1.

The wlroots family, Sway, Hyprland, River, Wayfire, and labwc, had a color-skew bug where blue rendered as brown. The capture bridge assumed every frame came in as BGRx, and Sway actually delivers Xbgr8888. I found it on Sway, but the fix lives in the shared portal-generic bridge, so it reaches all five compositors at once, not just the one where it turned up.

COSMIC was delivering zero frames compared to the last release, which is about as broken as it gets, and that's fixed now. It also gained real interactive input for the first time, through /dev/uinput, as an interim path while COSMIC's own input portal finishes upstream. That's a genuine step forward: COSMIC moves from video-only to something you can actually click and type into. The long-term fix is upstream in cosmic-comp, which is actively landing its own EIS support, and once that lands the uinput path can retire.

And across all of it, libei is now the default input protocol, which is where the whole ecosystem is heading anyway. The platform compatibility page has the current state for every desktop, kept up to date as this keeps moving.

Under the hood

A few changes that don't show up as a feature but matter to how the project runs: The license moved to BUSL-1.1 under Lamco Development LLC, following the LLC's own formation. The minimum Rust version moved to 1.89, on the 2024 edition. The IronRDP fork moved off my personal account onto the organization and got rebased onto current upstream. lamco-pipewire and xdg-desktop-portal-generic are now standalone crates on crates.io instead of vendored copies. And OpenH264 on Flatpak now correctly reflects reality: The old freedesktop extension that used to carry it is retired. There's no bundling and no auto-download now, since either one would violate Cisco's own license terms. Hardware encoding through VA-API or Vulkan runs first, and if you want software encoding on Flatpak, you install Cisco's binary yourself, with clear guidance on how.

One more thing worth naming plainly: The biggest single area of change in this whole window, by lines touched, was src/qemu. That's because lamco-rdp-server and a separate internal hypervisor-console product now build from one shared source tree instead of two copies that used to drift apart. I won't go into what that other product does here. That's a different post entirely. But the reason the codebase got restructured is worth saying: The same fix used to have to land twice, in two places, and sometimes it didn't. One tree means one fix, once. That's real engineering discipline, and it's part of why this window carried the weight it did.

The bug hunts

PAM under hardening was the most dramatic one, and I already told the shape of it above: a security-hardening pass silently made every PAM login fail, in exactly the case where PAM matters most. That one's fixed, and it's the reason this release exists at all in its current form.

The AVC444 disconnect bug is a smaller, tidier story. One file, ffi_types.rs, had a constant set to 1 when it should have been 0, the same default FreeRDP uses. That single wrong value made the Main and Aux AVC444 video streams disagree with each other, and Microsoft's own Remote Desktop client would drop the connection the moment you dragged a window. The fix was one line. Finding it took reading the EGFX spec side by side with the code until the mismatch fell out.

And there's a smaller story worth including on its own, the kind that shows the real texture of the work: I tried defaulting ZGFX compression to always on and reverted it. The compression variant in use degrades badly on data that's already compressed, like H.264 video, and it was stalling the pipeline on large keyframes. Sometimes the right fix is trying the obvious thing and then undoing it once the numbers say no.

A long tail of smaller fixes rode along with all of that: A DMA-BUF fallback for virtual GPUs, several EIS timing and panic fixes, clipboard MIME parsing and FUSE edge cases, IPv6 dual-stack listening finally works, FreeRDP 3.x TLS negotiation compatibility, and something like three dozen GUI fixes, including one where several text fields were silently discarding whatever you typed into them.

The parallel engine: What's happening upstream

None of this happens in a vacuum, and I want to say plainly what usually stays in the background. I'm the largest contributor to IronRDP by any measure, because I want it to be the premier RDP toolkit on Linux, and the numbers back that up: 72 merged pull requests as of this release, with roughly 20 more open. That's not drive-by patching. It's fuzzing infrastructure that found real bugs before anyone shipped them, a selectable TLS crypto provider, the transport redesign that directly shaped this release's own transport layer, and a robustness pass on the multitransport connector that caught a real packet-detection bug nobody else had caught, because nobody else had written the test for it. ClearCodec in IronRDP is original Rust work I contributed, not a port of anyone else's code, and the placement of the newer NSCodec work followed an architecture review I pushed for.

It doesn't stop at IronRDP. The portal layer got a merged pull request in xdg-desktop-portal itself, a stream property that KDE signed off on and that's now shipping upstream, plus backend updates merged into the wlroots portal and filed into the Hyprland one, and a merged fix in ashpd, the Rust bindings almost every portal-based tool on Linux depends on. Further down, Smithay merged a Wayland capture protocol I implemented, and I'm in the middle of proposing new capture-timing semantics directly in wayland-protocols, with a wlroots reference implementation already in flight. On the input side, reis, the library behind libei everywhere, has had real design collaboration and merged work from me too. And a clipboard bug I found in KDE's own portal got fixed upstream.

None of that work sits off to the side. The portal work underlies this release's own session-health monitoring. The IronRDP transport and TLS work is what shaped the new transport layer here. The libei work is why COSMIC could move past video-only and why libei became the default everywhere. This release is one visible surface of a much bigger, continuous push, and I wanted that on the page instead of buried in a pull request list somewhere.

Where it's going

The next real milestone is v1.5.0, reserved for multicodec support, the actual protocol-level change that will finally justify moving the version number instead of just the internals. That's the plain answer to why this one stayed 1.4.4. On the compositor side, COSMIC's upstream EIS work is the one to watch. Once it lands, the uinput workaround retires and COSMIC gets a real input path of its own.

So that's 1.4.4, the fifth release since I first put lamco-rdp-server out into the world in January. From where you're standing, it probably just looks like a small release. I promise you, underneath, it was a lot of paddling. Grab a build from the product page or straight from GitHub, and see for yourself.

GL
Greg Lamberson

Founder of Lamco Development LLC. Building Linux infrastructure: a Wayland-native RDP server, a memory-safe UEFI bootloader, and open-source Rust and Kotlin libraries.

More about Greg ›