NereusSDR rendezvous

On this page
  1. What runs where
  2. Self-hosting
  3. Beside a website
  4. Limits
  5. Memory
  6. Data use
  7. Rotating the secret
  8. What the logs contain
  9. Updating
  10. The checks

The rendezvous lets NereusSDR (the desktop's remote window and the iPhone app) reach a Core it cannot address directly, and carries pairing when the two are not on one network. It has three parts, and a server running it has all three:

NereusSDR uses rv.nereussdr.com, on a server of its own. Anyone can run their own; this file is the whole recipe. Every default in the scripts is nereussdr.com's, and each one is a setting.

What runs where

Program Listens on Reached from
Caddy (TLS, the WebSocket's front) TCP 80 and 443 the internet
The service TCP 8710 on 127.0.0.1 and ::1 Caddy only
The WebSocket relay the Unix socket /run/nereus-relay/relay.sock (mode 0660, group caddy) Caddy only (/v1/relay)
coturn (STUN and TURN) UDP 3478 and 443, on the public IPv4 and IPv6 the internet
coturn's relays UDP 61000 to 65535, same addresses the internet

coturn uses no TCP port at all, and Caddy none of UDP: Caddy's HTTP/3 (which would take UDP 443) is switched off. The relay range sits above Linux's ephemeral ports (32768 to 60999), so a relay never collides with another program's outgoing connection. The WebSocket relay opens no port of its own to the internet: it rides Caddy's TCP 443 under the same name.

Self-hosting

You need a server of its own for the rendezvous: Ubuntu 24.04 with a public IPv4 and a public IPv6 address on its network interface (a typical VPS; behind a provider's NAT, coturn would also need its external-ip setting, which this recipe does not cover), a domain whose DNS you control, SSH to the server as root, and rsync on your own computer (macOS has it). For up to 2000 Cores, 2 GB of memory is recommended; 1 GB carries them in normal use, and loses the service for a few seconds at a time if someone deliberately fills its connections (the rendezvous document, section 9.1). The commands below use example.org; use your own names.

1. DNS

Type Name Value Why
A rv.example.org the server's IPv4 the service, over IPv4
AAAA rv.example.org the server's IPv6 the service, over IPv6
A rv4.example.org the server's IPv4 the relay, IPv4 only
AAAA rv6.example.org the server's IPv6 the relay, IPv6 only

rv4 has no AAAA record and rv6 no A record, on purpose. The relay needs both address families (a client on an IPv6-only mobile network needs an IPv6 relay, and one on an old IPv4-only network an IPv4 one), and the ICE library in NereusSDR looks up one address for each relay name, preferring IPv4. So each family gets a name of its own, and the service hands out both, IPv4 first. The service's own name has both records.

Caddy obtains the certificate for rv.example.org from Let's Encrypt on its own once the A and AAAA records point at the server and TCP 80 and 443 are open, so set DNS up first. The relay names need no certificate.

2. Firewall

If the server has a firewall, open these, SSH first, so you do not lock yourself out:

ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw allow 3478/udp
ufw allow 443/udp
ufw allow 61000:65535/udp
ufw enable

(With a cloud provider's firewall instead, open the same ports there, for both IPv4 and IPv6.) Nothing else needs to be reachable: the service and the WebSocket relay listen on loopback and on a Unix socket only. setup-server.sh never changes the firewall.

fail2ban for SSH

fail2ban for SSH is recommended on the server. It is optional: setup-server.sh does not install it.

apt install fail2ban

Ubuntu's package enables the sshd jail with the systemd backend on install.

The Ubuntu 24.04 trap: sshd runs as ssh.service, but fail2ban 1.0.2's sshd filter matches _SYSTEMD_UNIT=sshd.service, so the stock jail never sees a failed login. The fix, observed on the live server on 2026-09-26: create /etc/fail2ban/jail.d/nereus-sshd.local with exactly

# Ubuntu 24.04 runs sshd as ssh.service; the stock filter matches sshd.service and would see nothing.
[sshd]
enabled = true
journalmatch = _SYSTEMD_UNIT=ssh.service + _COMM=sshd

then fail2ban-client reload, and check with fail2ban-client get sshd journalmatch and fail2ban-client status sshd (a failed login from another machine shows as "Currently failed: 1"). Defaults are 5 failures, a 10 minute ban. With password login off (key only), this mostly quiets the log and slows scanners.

Do not point fail2ban at the rendezvous service or coturn. Cellular carriers put many phones behind one shared IPv4 address, so a ban would cut off every user on it; the service's log leaves out addresses on purpose, so there is nothing to match; and the misuse that matters there (too many connections, introductions or relay slots) is already limited by the service and coturn per network, which "Limits" below describes.

3. Copy the files to the server

From a checkout of NereusSDR on your own computer:

ssh root@rv.example.org rm -rf /root/rendezvous
scp -r rendezvous root@rv.example.org:/root/rendezvous

(The rm first: copying onto an earlier copy would put the new one inside it, as /root/rendezvous/rendezvous, and leave the old files in use.)

4. Settings

On the server, as root, name your hosts, how many relays may run at once, your server's monthly transfer allowance in GB (see "Limits" and "Data use" below), and the SSH public key of the account deploy.sh will publish the code as (setup-server.sh makes that account, nereusrv, with exactly this key):

export RV_HOST=rv.example.org
export RV_RELAY_HOST4=rv4.example.org
export RV_RELAY_HOST6=rv6.example.org
export RV_RELAY_SLOTS=128
export RV_TRANSFER_GB_PER_MONTH=1000
export RV_DEPLOY_KEY='ssh-ed25519 AAAA...your key... you@computer'

setup-server.sh finds the rest itself; set RV_PUBLIC_IPV4 and RV_PUBLIC_IPV6 if it picks the wrong public addresses, RV_DATA_USE_INTERFACE to count data use on another interface than the default route's, RV_MEMORY_MB to size the memory limits for another amount than the server reports, and RV_WS_RELAY_SLOTS for how many WebSocket relay sessions run at once (16 by default; see "Limits"). Every value that belongs to one server is one of these settings, so the same files move to another server unchanged.

5. Set up the server

As root, check first, then set up:

apt-get update && apt-get install -y python3   # already there on Ubuntu server images
bash /root/rendezvous/deploy/setup-server.sh --dry-run
bash /root/rendezvous/deploy/setup-server.sh

setup-server.sh:

Run again, it reloads Caddy after a Caddyfile change (open connections are kept) and restarts a service only when one of its files changed (a coturn restart drops every relay in use); the dry run says which it would do. A run with --no-start starts nothing and keeps the reloads and restarts its changes call for in /etc/nereus-rendezvous/pending-actions (root only); the next run without it carries them out and the dry run lists them, so a check with --no-start before the real run loses none. It refuses to go on while another program holds UDP 3478 or 443.

6. The service's code

From your own computer, publish the code as the deploy account. With an entry for it in your ~/.ssh/config (for example Host nereus-rv, with HostName rv.example.org, User nereusrv and your key), deploy.sh's default target works as it is; without one, name the target:

NEREUS_RV_TARGET=nereusrv@rv.example.org:/opt/nereus-rendezvous/ rendezvous/deploy.sh --dry-run
NEREUS_RV_TARGET=nereusrv@rv.example.org:/opt/nereus-rendezvous/ rendezvous/deploy.sh

Then, as root on the server, start the service and the WebSocket relay (and after every later deploy):

systemctl restart nereus-rendezvous nereus-relay

7. Check it

From anywhere:

curl -sS https://rv.example.org/            # "This address is the NereusSDR connection service..." (426)
curl -sS https://rv.example.org/v1/relay    # the same text, from the WebSocket relay (426)
turnutils_stunclient -p 3478 rv4.example.org   # your address, over IPv4
turnutils_stunclient -p 443 rv6.example.org    # the same over IPv6

On the server: systemctl status caddy coturn nereus-rendezvous nereus-relay, and ss -lntup shows coturn on UDP 3478 and 443 only, the service on 127.0.0.1:8710 and [::1]:8710, and Caddy on TCP 80 and 443; the WebSocket relay holds no port, and ls -l /run/nereus-relay/relay.sock shows its socket as srw-rw---- with group caddy.

Then point NereusSDR at rv.example.org in its remote access settings.

Every request to rv.example.org goes to the service, except /v1/relay and what is under it, which goes to the WebSocket relay; each upgrades a WebSocket and answers anything else with 426, a short text and Upgrade: websocket. No matcher picks WebSockets out in Caddy, because Caddy compares header values exactly and Apple's WebSocket client sends Upgrade: WebSocket. Clients open the WebSocket over HTTP/1.1 (the rendezvous document, section 2): Caddy offers no WebSockets over HTTP/2.

Beside a website

The rendezvous can share a server with a website that Caddy already serves, but it needs care, which is why nereussdr.com gives it a server of its own:

Limits

The relay is sized by slots: RV_RELAY_SLOTS (128 by default) is how many relays (coturn allocations) run at once. From it:

Transfer is watched, not capped: see "Data use". To change the size, set RV_RELAY_SLOTS and run setup-server.sh again (it restarts coturn, which drops the relays in use).

The service's own limits (connections, rates, sizes) are in server/rendezvous.conf.sample, with the reasons in the rendezvous document, section 9: up to 2000 registered Cores and 256 other connections at once.

The WebSocket relay is sized by its own slots: RV_WS_RELAY_SLOTS (16 by default) is how many sessions (a Core and a device, one connection each) it carries at once, about as many as coturn's 128 slots carry relayed at both ends, and at most 2 for any one Core (as coturn's 8 relays per Core are about two sessions). A session carries control and media in two lanes, each allowed 80000 bytes a second each way (coturn's max-bps), so a burst of one never slows the other; each connection sends one datagram of up to 1500 bytes at a time, and a connection that falls behind loses its oldest datagrams rather than piling them up. A session with nothing to carry for 30 s ends, and so does one whose other end has been gone 30 s. The rest (connections per network, waiting connections) is in server/relay.conf.sample, with the reasons in the rendezvous document, section 12.5. Python on one vCPU carries these 16 comfortably by the prototype's figures; measuring the real server is still to come.

Memory

setup-server.sh works the memory limits out from the server's memory (RV_MEMORY_MB, by default what the kernel reports), in one formula: 256 MiB is kept for the system and coturn, Caddy's GOMEMLIMIT is half of the rest and the service's MemoryMax 35% of it, leaving 15% as headroom. On a 1 GB server that is 352 MiB for Caddy and 246 MiB for the service; on 2 GB, 855 and 598. With 2000 idle Cores and a full client pool the service holds about 86 MiB and Caddy about 270 MiB, measured.

The WebSocket relay's MemoryMax comes from its slots instead: 32 MiB and 2 MiB a slot, 64 MiB at 16 slots, out of the 15% headroom. Measured, it holds about 34 MiB with every one of its 16 sessions stalled at once.

If memory runs short, the kernel ends the WebSocket relay first, then the service (they have the higher OOM scores), which also frees the connections Caddy holds for them; systemd starts each again at once, the Cores register again and relay connections join again. Caddy restarts on its own after any failure. The rendezvous document, section 9.1, has the measurements and the arithmetic at 1 GB and 2 GB.

Data use

The relay's transfer is not capped. Instead, a small report watches it: nereus-data-use.timer runs nereus-data-use.service every hour, which reads how many bytes the server has sent on its network interface (the kernel's counter for the interface of the default route, or RV_DATA_USE_INTERFACE) and keeps the calendar month's total (UTC). Once a day it writes one line to the journal, and a warning line when the month's total has passed RV_TRANSFER_GB_PER_MONTH (1000 GB by default; set it to your plan's allowance):

journalctl -u nereus-data-use --since today

It counts everything the server sends on that interface (the relay, the service, updates), which is what a provider bills. What it cannot know: the kernel's counter starts again at every boot, so the bytes sent between the last hourly reading and a restart are lost (the line then says the total is short); bytes sent before the report was first installed in a month are not counted (the line says when it started counting); and the month is UTC's, which may not be the provider's billing month. It changes nothing on the server and needs no package beyond Python.

The WebSocket relay also counts what it carries itself: a journal line at the end of each session (how long it ran, how much it forwarded, and how many datagrams it dropped over the rate, from a full queue or with no one at the other end), and one a day with the day's total (journalctl -u nereus-relay | grep 'data use'). Those bytes are part of the server's total above as well.

Rotating the secret

The service and coturn share one secret, and the service and the WebSocket relay another. To replace both (for example if a file may have been read by someone else):

bash /root/rendezvous/deploy/setup-server.sh --rotate-secret

It writes new secrets and restarts coturn, the service and the WebSocket relay (--dry-run --rotate-secret says so first). The restart drops the relays in use; NereusSDR makes new ones with new credentials and grants. Registered Cores reconnect by themselves. The one backup it keeps of /etc/turnserver.conf holds the previous secret, readable by root alone, until the next change.

What the logs contain

All three log to the systemd journal only (journalctl -u nereus-rendezvous, journalctl -u nereus-relay, journalctl -u coturn); none writes a log file.

Updating

Pull NereusSDR. Publish the code first, then copy the tree again the same way as step 3 (remove the old copy first, or it nests) and run setup-server.sh with the same settings as step 4 (RV_DEPLOY_KEY may be left out once the account exists). The code goes first because setup-server.sh installs units and configuration for the code it finds in /opt/nereus-rendezvous: on a server set up before the WebSocket relay, it refuses to go on (in a dry run too) until deploy.sh has put the relay's code there, and then installs the relay's unit, starts it, and restarts the service onto its new configuration.

rendezvous/deploy.sh
ssh root@rv.example.org rm -rf /root/rendezvous
scp -r rendezvous root@rv.example.org:/root/rendezvous
ssh root@rv.example.org bash /root/rendezvous/deploy/setup-server.sh --dry-run
ssh root@rv.example.org bash /root/rendezvous/deploy/setup-server.sh

The dry run says whether Caddy would be reloaded or restarted and whether coturn, the service or the WebSocket relay would be restarted. setup-server.sh restarts only what had a file of its own change; when only the code changed, restart the two by hand: systemctl restart nereus-rendezvous nereus-relay.

The checks

These need Docker and run everything in ubuntu:24.04 containers, nothing on a real server: