A second RDP server in WSL, for testing¶
A client that is only ever tested against Windows is a client whose encoding
bugs stay hidden. Windows tolerates malformed PDUs that other servers reject,
and mstsc.exe and FreeRDP both skip optional structures, so a wrong encoder
can pass every Windows test and still be wrong.
xrdp in WSL is a cheap second opinion. It is strict about declared lengths, it
names the structure it was parsing when it refuses a PDU, and it speaks TLS, so
one xrdp instance exercises the whole path from X.224 to MCS Connect Initial.
That is how the RDP_CORRELATION_INFO defect was found: Windows negotiated fine
with a 24-octet block, and xrdp answered expected 35, remaining 23.
This page stands up that second server. It was written on Windows 11 with WSL 2, Ubuntu 26.04, XFCE and xrdp 0.10.1; the commands are written for Bash inside the distro and for PowerShell on the Windows side.
Before you start¶
| Requirement | Check |
|---|---|
| WSL 2 with a distro | wsl --list --verbose |
| Virtualisation enabled | BIOS/UEFI, and VirtualMachinePlatform on |
| Administrator rights | enabling the WSL features and winget installs |
Port 3389 is usually already taken on Windows by TermService, so this document uses 3390 throughout. Pick any port the host is not using.
1. Install a distro¶
wsl --list --online # what this host offers
wsl --install -d Ubuntu-26.04 # or Debian, Ubuntu-24.04, ...
wsl --list --verbose # expect VERSION 2 and State Running
If wsl --install refuses, the optional features are off and an elevated shell
is needed:
Start-Process powershell -Verb RunAs # accept the UAC prompt
First boot runs apt upgrade -y in the background and holds the dpkg lock. An
install attempted in parallel fails with:
E: Could not get lock /var/lib/apt/lists/lock. It is held by process 505 (apt)
Wait for it rather than killing dpkg mid-transaction:
while pgrep -x apt-get >/dev/null || pgrep -x dpkg >/dev/null; do sleep 20; done
2. Enable systemd¶
xrdp is a service, so it needs an init system. Ubuntu 24.04 and later already boot systemd as PID 1; check before changing anything:
ps -p 1 -o comm= # systemd -> nothing to do
If it prints init, add the setting and restart the distro:
printf '[boot]\nsystemd=true\n' >>/etc/wsl.conf
# from Windows: wsl --shutdown, then start the distro again
3. Choose a desktop¶
The desktop is what makes the RDP session worth looking at; xrdp only supplies the display. Lightweight options behave better inside WSL because a VM has less memory than a machine with a GPU.
| Desktop | Packages | Idle RAM | Notes |
|---|---|---|---|
| XFCE (recommended) | xfce4 xfce4-terminal |
~250 MB | Starts in about two seconds, uses startxfce4, smallest dependency set of the full desktops |
| LXQt | lxqt |
~200 MB | Also light; startlxqt |
| MATE | mate-desktop-environment |
~350 MB | GNOME 2 fork, conventional layout |
| openbox + xterm | openbox xterm |
~60 MB | Not a desktop. Enough to prove a session and see protocol errors |
| GNOME | gnome |
~700 MB | Works, but slower to start and heavier than useful here |
| KDE Plasma | kde-plasma-desktop |
~900 MB | Same; not recommended under WSL |
Install the one you want and the two packages every option needs:
apt-get update -qq
apt-get install -y --no-install-recommends xrdp xorgxrdp dbus-x11 fonts-dejavu-core
apt-get install -y --no-install-recommends xfce4 xfce4-terminal # or your choice
4. Create a login account¶
adduser rdpuser
# log in once with `wsl -d Ubuntu-26.04 -u rdpuser -- true` so the home is made
mkdir -p /home/rdpuser/.ssh # if you will use SSH instead
xrdp.ini ships with username=ask, so the client is asked for the account and
the password. Keeping ask avoids a password in a world-readable config file.
5. Point xrdp at the chosen desktop¶
/etc/xrdp/startwm.sh is what an authenticated session executes, so it
decides which desktop appears:
cat >/etc/xrdp/startwm.sh <<'EOF'
#!/bin/bash
unset DBUS_SESSION_BUS_ADDRESS
unset XDG_RUNTIME_DIR
export XDG_SESSION_TYPE=x11
export XDG_CURRENT_DESKTOP=XFCE
export XDG_SESSION_DESKTOP=XFCE
exec /usr/bin/startxfce4
EOF
chmod +x /etc/xrdp/startwm.sh
Substitute the command for your choice: startlxqt, mate-session,
startplasma-x11, or openbox-session. The unset lines matter: xrdp is not
a systemd session, and a stale bus address is the usual cause of a session that
starts and then dies.
6. Move xrdp off 3389¶
sed -i 's/^port=3389$/port=3390/' /etc/xrdp/xrdp.ini
sed -i 's/^port=3350$/port=3391/' /etc/xrdp/xrdp.ini
grep -E '^port=' /etc/xrdp/xrdp.ini
xrdp.ini has one listener per section: [xrdp1] is the login screen the
client connects to, and [Xorg] is the per-session display server. Both must
move. The TLS material xrdp presents by default is /etc/xrdp/cert.pem with
/etc/xrdp/key.pem; RDP does not chain-validate it, exactly like the
self-signed certificate a Windows host presents.
7. Start it and keep it started¶
systemctl enable xrdp xrdp-sesman xrdp-xorgxrdp
systemctl restart xrdp-sesman xrdp-xorgxrdp xrdp
systemctl --no-pager --lines=0 status xrdp
ss -ltn | grep 3390
8. Keep the VM alive¶
This is the single most common reason the lab appears to be down. WSL stops
the virtual machine when no distribution session is running, and xrdp dies with
it; mstsc then reports error 0x204 and Test-NetConnection returns
False. Hold a session open from Windows:
wsl -d Ubuntu-26.04 -u root -- sleep infinity
For mirrored networking, localhost works directly. In the default NAT mode it
does not: connect to the address the distro owns.
$ip = (wsl -d Ubuntu-26.04 -u root -- hostname -I).Trim() # e.g. 172.17.214.245
Test-NetConnection -ComputerName $ip -Port 3390
Then connect from the Windows side:
mstsc.exe /v:$ip:3390
9. Verify with the client, not only with mstsc¶
mstsc proves the desktop works. It does not prove the encoder does, so probe
the same server with this repository’s own tool:
dotnet run --project src/RdpClient.Cli -- --host $ip --port 3390 --tls-only
A healthy run reaches MCS and completes the channel join:
negotiated version 0x00000001, SelectedProtocol 0x00000001
Tls13, subject 'CN=...' (self-signed certificate accepted; ...)
sent MCS Connect Initial (561 bytes)
MCS accepted (conference 0), UserID 31219, ServerNodeID 31219
sent Erect Domain Request (12 bytes)
sent Attach User Request (8 bytes)
attached as MCS user 1012
joined MCS channel 1003
RESULT: MCS channel joined.
The last two lines are read from the server’s own Confirm PDUs rather than assumed, and they name the id the server assigned. The session then stops, because the next PDU is the Demand Active and the capability exchange is not implemented. xrdp’s log will show it hitting the client’s disconnect while reading the next channel join:
[ERROR] libxrdp_force_read: header read error
[ERROR] Processing [ITU-T T.25] ChannelJoinRequest failed
One accepted join followed by a read error on the next one is the expected end
of this stage. Anything earlier in the output is a real finding, as is any
GCC Conference Create Request client data field error or any
TS_UD_HEADER type ... is unknown line.
An earlier version printed MCS Connect Response result 3: connection
refused here. That was a misread TPKT octet rather than a refusal: the
parser started at offset 0 of the datagram and read the TPKT version 0x03 as
the MCS result. Suspect the parser before the server.
10. Read the server’s log before theorising¶
xrdp names the field it was parsing, which turns a guess into a measurement:
tail -40 /var/log/xrdp.log
tail -40 /var/log/xrdp-sesman.log
journalctl -u xrdp -n 30 --no-pager
The message that found the correlation block defect:
[ERROR] Parsing [MS-RDPBCGR] RDP_NEG_CORRELATION_INFO Not enough bytes in the stream: expected 35, remaining 23
The Windows equivalent is the RdpCoreTS/Operational log described in
AGENTS.md.
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
mstsc: 0x204, Test-NetConnection false |
the VM was stopped when the distro had no session | hold wsl ... -- sleep infinity open |
No connection could be made ... actively refused |
same as above, seen from rdpprobe |
same as above |
E: Could not get lock /var/lib/apt/lists/lock |
the first-boot apt upgrade still running |
wait for apt-get and dpkg to exit |
localhost:3390 unreachable from Windows |
NAT networking does not forward it | use the distro address, or set networkingMode=mirrored in %UserProfile%\.wslconfig |
| session starts, then the window closes | stale D-Bus address in the session | unset DBUS_SESSION_BUS_ADDRESS in startwm.sh |
xrdp_sec_incoming: xrdp_iso_incoming failed |
the PDU was refused during negotiation | read the error above it in /var/log/xrdp.log |
| everything works, then the VM vanishes | WSL idle shutdown | see step 8 |
Turning the lab off¶
systemctl stop xrdp xrdp-sesman xrdp-xorgxrdp
systemctl disable xrdp xrdp-sesman xrdp-xorgxrdp
Or remove the distro entirely with wsl --unregister Ubuntu-26.04, which
deletes its filesystem and every file inside it.
Security notes¶
xrdp listens on port 3390 of the distro. In NAT mode that address is reachable from the Windows host and from anything that can route to the VM, so do not port-forward it. The certificate xrdp presents is self-signed and RDP does not validate it, which is normal for RDP and is not a signal of a working authentication path.