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
- Never put RDP on the internet. Let the RDP server listen on 127.0.0.1 only (for xrdp:
port=tcp://.:3389in/etc/xrdp/xrdp.ini) and reach it through SSH, which already handles keys, authentication and encryption. - Keep the gateway on 127.0.0.1, on both ends.
websockify 3390 localhost:3389would offer the bridge to the whole network;websockify 127.0.0.1:3390 …does not. - Run the gateway only for the session. While it listens, any web page open in the same browser could connect to it. The RDP server still asks for its own password, but the gateway checks nothing.
- Never put a password in a command or a URL. The page ignores passwords in its URL.
Three ports, three jobs
| Port | What listens | Where |
|---|---|---|
| the page's | the web server that delivers this page (GitHub Pages, or your own) | anywhere |
| 3390 | the gateway, websockify: WebSocket in, TCP out. The page's Gateway field | 127.0.0.1 on your computer, forwarded by SSH |
| 3389 | the RDP server, xrdp or GNOME Remote Desktop | 127.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.
-o ServerAliveInterval=30 -o ServerAliveCountMax=3: ssh checks the connection every 30 seconds and gives up after 3 missed answers, so a dead network ends the tunnel within about 90 seconds instead of hanging, and the server cleans up websockify as above.-o ExitOnForwardFailure=yes: if port 3390 is taken on your computer (an earlier tunnel still runs), ssh stops with an error instead of logging in without the forward, which would leave a gateway that does not work.-J user@jumphost(add it if needed): the server is not reachable from your computer, only from another host you can log in to, and ssh hops through it. Everything else stays the same.
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 xrdpCheck: 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 see | What it means |
|---|---|
bind … Address already in use | Port 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 once | Nothing 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 directory | 2 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 session | The 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.