FreeRDP Web

Safety and tunnels

A browser cannot open TCP connections, so the page reaches the RDP server through a WebSocket gateway, websockify. SSH carries it to the server, so the RDP server never has to face the network.

This is a hobby project, not written by security professionals. Keeping your servers safe is up to you.

The rules

Three ports, three jobs

PortWhat listensWhere
the page'sthe web server that delivers this page (GitHub Pages, or your own)anywhere
3390the gateway, websockify: WebSocket in, TCP out. The page's Gateway field127.0.0.1 on your computer, forwarded by SSH
3389the RDP server, xrdp or GNOME Remote Desktop127.0.0.1 on the server

Start the gateway: pick one

Each runs in a terminal on your computer. Replace user@server; Ctrl-C in that terminal ends the tunnel and the gateway. Then open the page and connect with the gateway 127.0.0.1:3390.

1. Port → port: the normal port forward (recommended)

Install it once with the server's package manager (Debian and Ubuntu: apt install websockify; Fedora: dnf install python3-websockify). One line then starts the tunnel and the gateway:

ssh -tt -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o ExitOnForwardFailure=yes -L 127.0.0.1:3390:127.0.0.1:3390 user@server websockify 127.0.0.1:3390 127.0.0.1:3389

-tt always gives websockify a terminal on the server, even when ssh itself runs without one (from a script or a shortcut). When the connection ends, the server closes that terminal and websockify gets the hang-up signal and stops. Without a terminal it keeps running there. A connection that dies silently (the network gone) is noticed by the server after its keepalive timeout, and then cleaned up the same way.

If websockify already runs on the server's 127.0.0.1:3390 (a service you set up), only forward the port:

ssh -N -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o ExitOnForwardFailure=yes -L 127.0.0.1:3390:127.0.0.1:3390 user@server

No websockify on the server at all? The repository's rdp-gateway.sh --local user@server forwards the RDP port over SSH and runs websockify on your computer instead.

Advanced: 2. port → socket, 3. socket → socket Unix sockets on the server

In 1, websockify listens on the server's 127.0.0.1:3390 and xrdp on 127.0.0.1:3389. Loopback keeps the network out, but not the server's other local users: any account on that machine can reach both ports. A Unix socket is a file with an owner and a mode, so only the users it allows can open it. Your computer's side stays 127.0.0.1:3390, the page's Gateway field, in both.

Once, on the server: xrdp on a socket

xrdp moves from its TCP port to a socket in a directory of your group (the setgid bit 2 gives the socket that group). 2 and 3 both need it.

echo "d /run/xrdp-ws 2750 root $(id -gn) -" | sudo tee /etc/tmpfiles.d/xrdp-ws.conf
sudo systemd-tmpfiles --create /etc/tmpfiles.d/xrdp-ws.conf
sudo sed -i 's#^\( *\)port=\(tcp://[^ ]*:\)\{0,1\}3389 *$#\1port=unix://./run/xrdp-ws/xrdp.socket#' /etc/xrdp/xrdp.ini
sudo systemctl restart xrdp

Check: ls -l /run/xrdp-ws/xrdp.socket shows srw-rw---- root <your group>, and ss -tln no longer lists 3389.

2. Port → socket: websockify on 127.0.0.1:3390, xrdp on the socket

xrdp listens on no TCP port any more. websockify still does while the tunnel runs, and it opens the socket as you, so during your session the server's other users could still reach xrdp through it; 3 closes that.

ssh -tt -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o ExitOnForwardFailure=yes -L 127.0.0.1:3390:127.0.0.1:3390 user@server websockify 127.0.0.1:3390 --unix-target=/run/xrdp-ws/xrdp.socket

3. Socket → socket: nothing listens but sshd

websockify listens on a socket too, in /run/user/<your ID> (mode 0700, yours only). The line runs in bash or zsh on your computer (Linux, macOS, WSL): the first ssh asks the server for your user ID, so it is never typed by hand; without an SSH key it asks for the password twice. Not in /tmp: anyone can create files there, and a socket another user created first under that name would receive your session.

S=user@server; R=$(ssh "$S" id -u) && ssh -tt -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o ExitOnForwardFailure=yes -L 127.0.0.1:3390:/run/user/$R/rdp-ws.sock "$S" websockify --unix-listen=/run/user/$R/rdp-ws.sock --unix-target=/run/xrdp-ws/xrdp.socket

When something goes wrong

You seeWhat it means
bind … Address already in usePort 3390 is taken on your computer: an earlier tunnel still runs. Find it with ss -ltnp | grep 3390 (Windows: netstat -ano | findstr 3390), or use another port on both sides and in the Gateway field.
Chrome asks to “access other apps and services on this device”Allow it. That is how a page from a web site reaches the gateway on 127.0.0.1 (Chrome's Local Network Access protection). Refused, the console shows ERR_BLOCKED_BY_LOCAL_NETWORK_ACCESS_CHECKS.
The connection fails at onceNothing listens on the gateway port, or the server's RDP port is not 3389. The page's Log (under the form) shows the stage that failed.
channel … open failed: … No such file or directory2 or 3: a socket is missing on the server. websockify did not start (is it installed?), or xrdp does not listen on /run/xrdp-ws/xrdp.socket.
websockify keeps running after the sessionThe tunnel was started without -tt, or the network dropped before the server noticed. Log in and stop it (pkill -f websockify).

What SSH does not cover

The client currently accepts any RDP server certificate (/cert:ignore). Inside an SSH tunnel to your own server's 127.0.0.1 that is the server you meant, because SSH authenticated the host. With Direct TCP or a gateway on another machine nothing checks the RDP server's identity yet.

Back to FreeRDP Web