Jump to content

Wsrelay on PMSS

From Pulsed Media Wiki


On a Pulsed Media seedbox, wsrelay runs in rootless Docker inside your account and answers at wss://SERVER.pulsedmedia.com/user-USERNAME/wsrelay/, behind your panel login. Tested on a Pulsed Media seedbox on 24 October 2026 with nicholashill/wsrelay 2.2: through the web address, a WebSocket handshake without the panel login got 401, with the panel login 101 (connected), and with a wrong origin 403. wsrelay is a WebSocket relay: by its documentation, web pages connected to it send messages to each other and save small pieces of data on it, so a page you host can share live state without you writing a server program. Sending messages was not part of the test.

wsrelay has no login of its own, and other accounts on the same server can connect to its port directly. Use it only for messages and data that would not matter if another customer on that server read or changed them.

Privacy

Level: MEDIUM. Using wsrelay reduces your privacy because other customers on the server can join it:

  • wsrelay has no login. Its only checks are the Origin header and that the client asks for a WebSocket subprotocol; by default any subprotocol name is accepted. The port on 127.0.0.1 is reachable by every account on the same server, and a program there can send any Origin it likes, so another customer can join the relay and read or send messages.
  • Realms (the relay's rooms) are plain numbers, not secrets. By the wsrelay protocol, any client can join a realm, message everyone in another realm, read saved data from any realm (<4,score in the protocol's own example) and overwrite or delete the data saved in a realm it joins (>score with no value deletes).
  • The panel login in front of the web address keeps out visitors from the internet. It does not cover the port.
  • No connection to an outside service was found in the project's documentation; the code was not audited.

The project's optional JWT login (see JWT.md) is "currently only enforced for deleting persisted realms", so it does not close the relay to other clients.

Requirements

Item Measured or required
Memory Not measured separately in the test.
Disk The image, about 49 MB compressed on Docker Hub. The install keeps no data volume.
Own login None. See Privacy.
Web address wss://SERVER.pulsedmedia.com/user-USERNAME/wsrelay/ through a proxy fragment with map-urlpath.
Other apps A web page that speaks the wsrelay protocol, for example through the project's client library.

Install

Run these over SSH as your own user. Replace SERVER with your server's name as it appears in your panel address, USERNAME with your user name, and PORT with a free port between 2000 and 38000; check with ss -tln that nothing listens on it. Run docker ps first: an empty list means Docker is running.

1. Make the folder and the settings file. ACCEPTED_ORIGINS is the address of the web page that will use the relay: https:// and the host name only, with no port, path or trailing slash, because wsrelay compares only those two parts. Separate several addresses with commas and no spaces. The test used the server's own address. Every account's pages on the server are served under that same address, so the origin check cannot tell your page from another customer's page there. Left out, the setting accepts every origin (upstream README).

mkdir -p ~/docker/wsrelay
chmod 700 ~/docker/wsrelay
cat > ~/docker/wsrelay/app.env <<'EOF'
ACCEPTED_ORIGINS=https://SERVER.pulsedmedia.com
EOF
chmod 600 ~/docker/wsrelay/app.env

2. Pull the tested image.

docker pull nicholashill/wsrelay@sha256:dbbe78ff89e20f9bcd41fe020c74cd9a981ab5c9d86b8c88a33236ae7cf12873

3. Start it on a loopback port. In the other app guides on this wiki, the app's own login is set before its port is published. wsrelay has none, so other accounts can reach the port from this first start.

docker run -d --name wsrelay --restart unless-stopped --env-file ~/docker/wsrelay/app.env \
  -p 127.0.0.1:PORT:22002 \
  nicholashill/wsrelay@sha256:dbbe78ff89e20f9bcd41fe020c74cd9a981ab5c9d86b8c88a33236ae7cf12873

Keep 127.0.0.1: in the -p option. docker logs wsrelay shows "Version 2.2", the accepted origin and "Listening on port 22002."

4. Add the proxy fragment as ~/.lighttpd/custom.d/wsrelay.conf, with your user name and port. It keeps the panel login: do not add auth.require = (), because wsrelay has no login to fall back on. The setenv line stops your web server from passing your panel login, which is your account password, on to wsrelay after checking it; this line was tested with ordinary web requests, not with a wsrelay connection.

$HTTP["url"] =~ "^/user-USERNAME/wsrelay/" {
  setenv.set-request-header = ( "Authorization" => "" )
  proxy.server = ( "" => ( ( "host" => "127.0.0.1", "port" => PORT ) ) )
  proxy.header = ( "upgrade" => "enable",
    "map-urlpath" => ( "/user-USERNAME/wsrelay/" => "/", "/user-USERNAME/wsrelay" => "" ) )
}

Check it with /usr/sbin/lighttpd -t -f ~/.lighttpd.conf. The web server picks the file up within a minute or two (how that works).

Configure

  • ACCEPTED_ORIGINS must match the page that opens the connection. In the test a handshake with another origin, or with none, got 403. For another origin the log says "Rejected connection from non-permitted origin" and names it.
  • Clients must ask for a WebSocket subprotocol. The test client asked for wsrelay; a handshake without any subprotocol got 403, even with the right origin. Any name is accepted by default. ACCEPTED_PROTOCOLS in the upstream README narrows the list (not tested), so the subprotocol is not a password.
  • Clients connect to wss://SERVER.pulsedmedia.com/user-USERNAME/wsrelay/ with your panel login; in the test curl sent it, and a page in a browser was not tried. The panel login opens your whole panel, so the relay is for your own pages and devices. Pulsed Media's terms say a personal account is used by its primary owner only and that access to its services may not be resold or shared; giving a service to someone free of charge is allowed, and you carry the responsibility for what they do.
  • Without a data volume, realm data that clients save stays inside the container and is gone when the container is removed. The upstream README shows mounting /app/data to keep it; that was not tested.

Integrate

The upstream documentation describes no integration with Sonarr, Radarr, Prowlarr or Jellyfin. Its clients are web pages. Pages in your /user-USERNAME/ folder are behind your panel login. Pages in the public folder (/public-USERNAME/) are open to the internet, so their visitors have no panel login and the relay answers them with 401. See web hosting.

Test

The handshake through the web address, from the server or your own computer; curl asks for the panel password:

curl -s -i -N --max-time 4 -u USERNAME https://SERVER.pulsedmedia.com/user-USERNAME/wsrelay/ \
  -H 'Connection: Upgrade' -H 'Upgrade: websocket' -H 'Sec-WebSocket-Version: 13' \
  -H "Sec-WebSocket-Key: $(head -c 16 /dev/urandom | base64)" \
  -H 'Sec-WebSocket-Protocol: wsrelay' -H 'Origin: https://SERVER.pulsedmedia.com' | head -1

The same check straight to the port on the server, which is what another account can do: run it without -u USERNAME and with http://127.0.0.1:PORT/ as the address.

Check Result in the test
docker ps wsrelay running
ss -tln | grep ':PORT ' Must show 127.0.0.1:PORT, never 0.0.0.0:PORT or *:PORT, which would put it on the internet
Web address, panel login, right origin, wsrelay subprotocol HTTP/1.1 101 Switching Protocols
Web address, no panel login 401
Web address, panel login, wrong origin 403
Straight to 127.0.0.1:PORT, right origin and subprotocol 101
Straight to 127.0.0.1:PORT, wrong or missing origin, or no subprotocol 403
Plain GET / straight to the port 404; wsrelay only answers WebSocket handshakes

Uninstall

Everything the install created belongs to your account, so plain commands remove it:

docker rm -f wsrelay
rm ~/.lighttpd/custom.d/wsrelay.conf
touch ~/.lighttpd/custom
rm -r ~/docker/wsrelay
docker image rm nicholashill/wsrelay@sha256:dbbe78ff89e20f9bcd41fe020c74cd9a981ab5c9d86b8c88a33236ae7cf12873

The touch line makes the web server restart without the removed fragment: deleting a file alone does not trigger the restart (details). Removing the container also deletes any realm data clients saved, because the install keeps no data volume.

Troubleshooting

  • The handshake gets 403. The page's origin is not in ACCEPTED_ORIGINS (check for a port or trailing slash in the setting), or the client did not ask for any WebSocket subprotocol. docker logs wsrelay names the rejected origin.
  • The handshake gets 401. The panel login is missing. That is the fragment working as intended.
  • A plain request gets 404. Normal: wsrelay answers only WebSocket handshakes. In the test a plain request straight to the port got 404; through the web address it was not tried.
  • 502 Bad Gateway. The container is not running or listens on another port; check docker ps and the port in the fragment.
  • The relay still answers after you removed the fragment. Run touch ~/.lighttpd/custom; the restart follows within a minute or two.

Not on PMSS shared hosting

A relay that other people use, such as a public chat or a game for your friends, does not fit a shared seedbox. Behind the panel login every player would need your panel password, which opens your whole panel. Without the panel login (auth.require = () in the fragment), the relay would be reachable from the internet at the same web address with only the origin check left, which any program can pass. For a public relay, run wsrelay on a VPS of your own; that setup was not tested.

For your own pages and devices no separate server is needed: rootless Docker starts automatically for every account on a Pulsed Media seedbox, next to your web folder.

See also