Skip to content

Install the Relay

The relay is a stateless QUIC forwarder for NAT traversal between edges. It carries opaque ciphertext only — relay operators can’t see your media. You only need a relay if your edge sites can’t reach each other directly (different ASNs, double-NAT, restrictive firewalls). Two edges on the same LAN, or edges connected over a site-to-site VPN, don’t need it.

  • A Linux host on a public IP, or behind a static port-forward of UDP 4433. Can share a box with the manager — the relay installs alongside under different paths (/opt/bilbycast-relay/, /etc/bilbycast/) so the two coexist cleanly.
  • About 5 minutes.

The relay is statically linked against musl, has no runtime dependencies, and runs on x86_64 and aarch64.

The relay listens on two ports:

PortProtocolSourcePurpose
4433UDP (QUIC / TLS 1.3)Every edge that pairs through this relayTunnel data plane. Override with --quic-addr.
4480TCP (HTTP)Manager host, your monitoring hostREST stats + /health. Optional — close it if you don’t query stats remotely. Override with --api-addr.

The relay itself connects outbound to the manager on TCP 8443 (wss://), so no inbound port is needed for control. If you front the relay with a load balancer (multiple relay instances for HA), the LB needs UDP/QUIC pass-through on 4433 — not TLS termination, since QUIC carries its own TLS 1.3.

QUIC binds on 0.0.0.0:4433 (every interface) by default — that’s what the relay listens on. Remote edges need a different value: the public address they dial to reach you. These two are distinct any time the relay sits behind NAT, a cloud-instance public-IP mapping, or a load balancer:

  • Bind address (quic_addr / quic_addrs) — the listen socket. 0.0.0.0:4433 is correct for most installs.
  • Advertised address (public_quic_addr) — what edges connect to. Set this to a hostname or IP edges can actually reach. The manager reads it from health and pre-populates the tunnel-creation dropdown. Without it, the manager can’t auto-fill a usable relay address for tunnel configs and the relay shows as disabled in the dropdown.

Prefer a DNS name when you have one (relay.example.com:4433). It survives Lightsail static-IP releases, cloud instance migrations, and lets you front a pool of relays behind one record. Edges resolve the name on every connect attempt, so an IP change behind the record is picked up automatically without a tunnel reconfig. Falls back to a raw IP literal cleanly when DNS isn’t an option (54.1.2.3:4433, [2001:db8::1]:4433).

Full network map: Deployment overview.

Terminal window
curl -fsSL -O https://github.com/Bilbycast/bilbycast-relay/releases/latest/download/bilbycast-relay-$(uname -m)-linux
curl -fsSL -O https://github.com/Bilbycast/bilbycast-relay/releases/latest/download/bilbycast-relay-$(uname -m)-linux.sha256
sha256sum -c bilbycast-relay-$(uname -m)-linux.sha256
# Rename to bilbycast-relay so subsequent commands are arch-agnostic
mv bilbycast-relay-$(uname -m)-linux bilbycast-relay
chmod +x bilbycast-relay

You should see bilbycast-relay-x86_64-linux: OK (or aarch64). The rename keeps the rest of this page concise; the canonical name is what the .sha256 file expects, so we verify under that name before moving.

Every release ships a Sigstore-signed manifest.json alongside the bare binaries. The sha256sum -c step above catches mid-transfer corruption; verifying the signature additionally proves the manifest was published by the Bilbycast release workflow on a tagged commit.

Install cosign — on Ubuntu / Debian the simplest path is the upstream static binary with SHA-256 verification:

Terminal window
COSIGN_VERSION=v2.4.1
case "$(uname -m)" in
x86_64) COSIGN_ARCH=amd64 ;;
aarch64) COSIGN_ARCH=arm64 ;;
*) echo "Unsupported architecture: $(uname -m)"; exit 1 ;;
esac
COSIGN_ASSET="cosign-linux-${COSIGN_ARCH}"
curl -fsSL -o /tmp/cosign \
"https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/${COSIGN_ASSET}"
expected="$(curl -fsSL "https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign_checksums.txt" | awk -v a="${COSIGN_ASSET}" '$2 == a {print $1}')"
got="$(sha256sum /tmp/cosign | awk '{print $1}')"
[[ -n "${expected}" && "${got}" == "${expected}" ]] || { echo "cosign checksum mismatch"; exit 1; }
sudo install -m 0755 /tmp/cosign /usr/local/bin/cosign && rm /tmp/cosign

Then verify the manifest:

Terminal window
curl -fsSL -O https://github.com/Bilbycast/bilbycast-relay/releases/latest/download/manifest.json
curl -fsSL -O https://github.com/Bilbycast/bilbycast-relay/releases/latest/download/manifest.sig.bundle
cosign verify-blob \
--bundle manifest.sig.bundle \
--certificate-identity-regexp 'https://github.com/Bilbycast/bilbycast-relay/.github/workflows/nightly-release.yml@refs/tags/v.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
manifest.json

A successful verify prints Verified OK. The same Sigstore-signed manifest drives the upgrade flow below, so this is the verifier’s main checkpoint.

The simplest deployment — useful for testing or when you don’t need the relay reporting back to the manager:

Terminal window
./bilbycast-relay

Defaults: QUIC on 0.0.0.0:4433, REST on 0.0.0.0:4480. Override with --quic-addr or --api-addr. Ctrl-C to stop.

In the manager UI:

  1. Go to Admin → Nodes, click + Add Node, pick device type Relay.
  2. Copy the one-shot registration token.

Next to the relay binary, write relay.json (replace REPLACE_WITH_YOUR_MANAGER_HOSTNAME, REPLACE_WITH_YOUR_RELAY_HOSTNAME, and <token-from-manager> with the real values — don’t paste this verbatim):

{
"quic_addr": "0.0.0.0:4433",
"api_addr": "0.0.0.0:4480",
"public_quic_addr": "REPLACE_WITH_YOUR_RELAY_HOSTNAME:4433",
"require_bind_auth": true,
"manager": {
"enabled": true,
"urls": [
"wss://REPLACE_WITH_YOUR_MANAGER_HOSTNAME:8443/ws/node"
],
"registration_token": "<token-from-manager>"
}
}

public_quic_addr is the address edges will dial — see Bind address vs advertised address above. Prefer a DNS name (e.g. relay.example.com:4433) over a raw IP. If the relay shares a host with the manager (typical for small deployments on a single cloud instance), neither the manager nor the relay can discover this from the WS connection alone — you have to set it explicitly. Unspecified values (0.0.0.0:4433, [::]:4433) are rejected at config load.

urls is an array (1-16 entries, each must be wss://). For a single manager that’s one entry; for an HA-paired manager cluster you’d list both hostnames — the relay tries them in order and rotates on WebSocket close with a 5-second backoff.

Launch:

Terminal window
./bilbycast-relay --config relay.json

For a self-signed manager cert (only relevant if you skipped ACME / Let’s Encrypt on the manager), add "accept_self_signed_cert": true inside the manager block and export BILBYCAST_ALLOW_INSECURE=1 before launching. The env var is a deliberate safety guard.

On first connect the relay swaps the registration token for a permanent node_id + node_secret, rewrites relay.json in place with those values (removing the now-spent registration token), and reconnects automatically going forward. Don’t be surprised when your relay.json looks different after the first boot — that’s the credential persistence working as intended. The file’s runtime user (bilbycast on the systemd install in step 4) must therefore be able to write it; step 4 sets the ownership accordingly.

For production, run the relay as a systemd service. Drop into /etc/systemd/system/bilbycast-relay.service:

[Unit]
Description=bilbycast-relay QUIC NAT traversal
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=bilbycast
Group=bilbycast
WorkingDirectory=/opt/bilbycast-relay
ExecStart=/opt/bilbycast-relay/bilbycast-relay --config /etc/bilbycast/relay.json
Restart=on-failure
RestartSec=2s
LimitNOFILE=65536
Environment=RUST_LOG=info
[Install]
WantedBy=multi-user.target

Then:

Terminal window
# `|| true` — bilbycast user may already exist from a manager install on this box
sudo useradd -r -s /sbin/nologin bilbycast || true
sudo mkdir -p /opt/bilbycast-relay /etc/bilbycast
sudo install -m 0755 -o bilbycast -g bilbycast bilbycast-relay /opt/bilbycast-relay/
# relay.json: bilbycast OWNS it (not root) — the relay rewrites this file on
# first connect to swap the registration_token for the permanent node_id +
# node_secret. Root-owned 0640 would block that write and you'd be stuck on
# next restart with a spent registration token.
sudo install -m 0640 -o bilbycast -g bilbycast relay.json /etc/bilbycast/relay.json
sudo systemctl daemon-reload
sudo systemctl enable --now bilbycast-relay
sudo systemctl status bilbycast-relay --no-pager

Expected: active (running). Logs: sudo journalctl -u bilbycast-relay -f.

Co-existing with the manager on the same box

Section titled “Co-existing with the manager on the same box”

If this box also runs the manager, the two installs occupy separate trees so they don’t collide:

ManagerRelay
Binary/opt/bilbycast-manager/bilbycast-manager/opt/bilbycast-relay/bilbycast-relay
Config + secrets/etc/bilbycast-manager/manager.env/etc/bilbycast/relay.json
Systemd unitbilbycast-manager.servicebilbycast-relay.service
Service userbilbycast (shared)bilbycast (shared)

Same bilbycast user owns both — useradd ... \|\| true above is idempotent.

The relay ships an operator-run upgrade script. It downloads the latest signed manifest.json + manifest.sig.bundle, verifies the Sigstore signature against the publishing workflow’s identity (auto-installing cosign with checksum verification if it isn’t already on the host), pulls the matching arch-specific binary (x86_64 / aarch64), verifies SHA-256 against the signed manifest, atomically swaps the binary with a .previous backup, restarts the systemd unit, polls /health, and auto-rolls back to the previous binary on a failed health probe.

The simplest path is curl-pipe-bash from the latest release:

Terminal window
curl -fsSL https://github.com/Bilbycast/bilbycast-relay/releases/latest/download/upgrade-relay.sh \
| sudo bash

Operators who’d rather review the script first can grab it once and re-run it as needed:

Terminal window
curl -fsSL -o upgrade-relay.sh \
https://github.com/Bilbycast/bilbycast-relay/releases/latest/download/upgrade-relay.sh
chmod +x upgrade-relay.sh
sudo ./upgrade-relay.sh # apply latest stable
sudo ./upgrade-relay.sh --dry-run # download + verify only; print plan
sudo ./upgrade-relay.sh --target-version 0.7.0 # pin to a specific tag

The relay is stateless — a restart drops connected edges, which all reconnect automatically. For zero-disruption upgrades, run multiple relay instances behind a load balancer and roll through them one at a time. Pass --help for every flag, including --service, --binary-path, --health-url, --health-timeout, --no-rollback, and --no-verify-cosign (for air-gapped boxes that can’t install cosign).

The script requires the systemd unit from step 4 — it reads systemctl cat bilbycast-relay to auto-detect the binary path. On a foreground-only install it errors out with systemd unit 'bilbycast-relay' not found. For a foreground install, do the swap by hand:

Terminal window
# 1. Stop the foreground ./bilbycast-relay process (Ctrl-C in its terminal).
# 2. Backup the running binary so you can roll back if needed.
rm -f bilbycast-relay.previous
mv bilbycast-relay bilbycast-relay.previous
# 3. Re-run step 1's download block to fetch + verify the new binary
# into CWD (the final `mv … bilbycast-relay && chmod +x` lands it
# next to the .previous backup).
# 4. Restart.
./bilbycast-relay --config relay.json
# Rollback (if the new version misbehaves):
# mv bilbycast-relay.previous bilbycast-relay

The single-host systemd install above is the right shape for most deployments. For larger or more redundant setups:

  • Multiple relays behind a load balancer — the relay is stateless, so an LB doing UDP/QUIC pass-through on 4433 across several relay instances gives you horizontal scale + zero-disruption upgrades. Roll one relay at a time using the upgrade script above; edges reconnect transparently.
  • Geographic redundancy — run a relay in each region; edges can be configured with multiple relay candidates and will fail over on tunnel loss.
  • Relay security — bind tokens, end-to-end tunnel encryption, why operators can’t see media.