Repository navigation
deploy proxy
Deploy your own proxy relay on any Linux VPS. You own the node, you run the node. LightSpeed also ships a community relay network that works out of the box, but this guide is for running your own dedicated node. This is self-hosting, full stop.
A proxy node is a small relay server that sits between your game client and the game server. Your client tunnels game packets to the node over UDP, and the node forwards them toward the game server over its own high-speed backbone connection. The result is a shorter, less congested path than your ISP's default route, which usually means lower and more stable ping.
The model is self-hosted: you provision a VPS, install the proxy binary, and point your client at it. Nobody else uses your node, and you don't rely on anyone else's. Run one node for yourself, or spin up a few in different regions and let the client pick the fastest automatically (see Multi-Node Mesh). If you would rather not run your own, the client uses the community relay network by default (see Community Relay Network).
A single node is tiny, roughly ~500KB of actual RAM, so it fits comfortably on even the smallest VPS.
The full deployment options (native binary, Docker, automated script) are documented in depth in infra/README.md. This guide gives you the essentials and the operational details you need day to day. Don't duplicate what infra already covers; follow the links.
| Port | Protocol | Purpose |
|---|---|---|
4434 |
UDP + TCP | Data plane: relayed game traffic (UDP, or TCP via the --tcp tunnel) |
4433 |
QUIC | Control plane: client registration and auth |
8080 |
HTTP | Health check and Prometheus metrics |
All three ports are configurable via the [network] section - see Configurable ports.
Before you start, you need:
- A Linux VPS. Any provider works. A single small instance is plenty.
- SSH access to the VPS.
-
Open firewall ports. Allow inbound UDP
4434(data) and TCP4434(TCP tunnel), UDP4433(QUIC control), and TCP8080(health/metrics). Ports are configurable via the[network]section - see Configurable ports. The exact commands depend on your provider's firewall andufw/firewalldon the box. - A way to run the proxy. Either the Rust toolchain to build from source, or Docker. See the options below.
There are three supported ways to get a node running. Full, step-by-step instructions for each live in infra/README.md. Here's the short version.
Relays use a versioned release layout so a bad binary can always be rolled back:
/opt/lightspeed/releases/<version>/lightspeed-proxy # one immutable dir per version
/opt/lightspeed/current -> releases/<version> # active release (symlink)
The systemd unit runs /opt/lightspeed/current/lightspeed-proxy, so switching versions is an atomic ln -sfn repoint, not an in-place file swap. The three most recent releases are kept; older ones are pruned only after a new release is activated and passes its health gate.
For a fresh node, use the one-shot setup script (it uploads the binary, writes the config and unit, installs the first release, and verifies health):
# Build locally (the quic feature is required for the control plane)
cargo build --release -p lightspeed-proxy --features quic
# Provision the node (ip, node-id, region)
bash infra/scripts/setup-new-node.sh YOUR_VPS_IP relay-1 us-eastThe same layout is used under the hood by relay-install.sh, which you can also run directly on a node to install a staged binary:
# On the relay, with a staged binary already on disk
bash relay-install.sh --binary /tmp/lightspeed-proxy.staged --version 20260101T000000Z-abc1234relay-install.sh verifies the file is an executable ELF for the host arch, runs --check against /etc/lightspeed/proxy.toml, repoints current, restarts the service, and polls /health for up to 15s. If the new release never becomes healthy it repoints current back to the previous release, restarts, and exits non-zero.
This is the recommended path: ~500KB RAM, sandboxed with DynamicUser, ProtectSystem=strict, and NoNewPrivileges.
# Pull from GitHub Container Registry
docker pull ghcr.io/shibbityshwab/lightspeed-proxy:latest
docker run -d --name ls-proxy \
-p 4434:4434/udp -p 4434:4434/tcp -p 4433:4433/udp -p 8080:8080 \
ghcr.io/shibbityshwab/lightspeed-proxy:latest
# Or build from source
docker build -f infra/docker/Dockerfile -t lightspeed-proxy .
docker run -d --name ls-proxy \
-p 4434:4434/udp -p 4434:4434/tcp -p 4433:4433/udp -p 8080:8080 \
lightspeed-proxyNote the control plane port 4433 is published alongside the data plane, and the data port is published for both UDP and TCP (the TCP listener supports the --tcp tunnel). The infra/README.md Docker examples predate the control plane; make sure you publish all ports so registration and the TCP tunnel work.
Configure your nodes and run the deploy script:
export LIGHTSPEED_NODES='{"relay-1":{"ip":"1.2.3.4"},"relay-2":{"ip":"5.6.7.8"}}'
./infra/scripts/deploy.shThe project's own relays are deployed through the Deploy Proxy GitHub
Actions workflow (.github/workflows/deploy.yml), which builds the proxy and
runs deploy.sh on every push to master that touches proxy/**,
protocol/**, or the deploy scripts. The proxy must be built with
--features quic for the control plane to work.
The proxy has a QUIC control plane (port 4433) that gates the data plane. As of
v1.1.0, require_auth defaults to true: the proxy will not relay data-plane
packets from a client until that client has registered over the QUIC control plane.
The control plane is QUIC with mTLS. The flow is:
- A client connects to the QUIC control port (
4433) and sends aRegistermessage. - The proxy checks capacity, generates a random
session_token, and records the client's IP. - The proxy replies with a
RegisterAckcarrying thesession_token. - The client stamps that token into every data-plane packet header.
- The proxy validates an (IP, token) pair on every data-plane packet, dropping any that do not match.
This raises the bar for abuse: a random UDP sender cannot relay through your node without
first completing a QUIC registration. The token is 8-bit (defense-in-depth alongside the
IP binding); the enforcement path is covered by proxy/tests/integration_security.rs.
- The proxy must be built with the
quicfeature (the Docker image and release binaries already are). - The client must be built with the
quicfeature so it can register (the release binaries already are). A client built withoutquicsends token0and will be rejected by an authenticated proxy - build withcargo build --features quic.
[security]
require_auth = true # defaultTo disable auth (dev mode only, not recommended for a public node), set it to false.
The proxy prints the auth state at startup, so you can confirm the setting took effect:
Auth enabled: true
All three ports are configurable via a [network] section in proxy.toml:
[network]
data_port = 4434 # UDP + TCP data plane
control_port = 4433 # QUIC control plane
health_port = 8080 # HTTP health/metrics
# TCP tunnel (client→proxy over TCP)
tcp_enabled = true
tcp_max_connections = 256
tcp_read_timeout_secs = 10The --data-bind, --control-bind, and --health-bind CLI flags override the config
(e.g. to bind a specific interface instead of 0.0.0.0).
For networks that block or throttle UDP, the client↔proxy leg can run over TCP instead.
On the client, pass --tcp (or set tunnel.transport = "tcp" in the client config); the
proxy accepts both UDP and TCP on its data port by default. The same auth, rate-limit,
abuse, and FEC pipeline applies to TCP traffic - it is not a weaker path. The TCP tunnel
uses length-prefixed framing with a hard frame-size cap and connection limits.
For best results, deploy 2-3 nodes in different regions. The client probes every configured proxy on startup and auto-selects the fastest route, so more regions means a better chance of a low-latency path to your game server.
RTT depends on your location, ISP, and the game-server region, so measure it
from your own connection instead of trusting a fixed table. Run
lightspeed --probe-proxies to probe every relay in the signed registry and
see real numbers for your network, then pick the region closest to the game
server.
Add all your proxies to the client config:
[proxy]
servers = [
"YOUR_SGP_IP:4434",
"YOUR_LAX_IP:4434",
]The client probes all proxies on startup and auto-selects the fastest route.
Every node exposes two HTTP endpoints on port 8080:
# Health check
curl http://YOUR_PROXY_IP:8080/health
# Prometheus metrics
curl http://YOUR_PROXY_IP:8080/metrics/health returns a small JSON document with the node's status, uptime, active sessions, and total packets relayed. /metrics exports Prometheus-format metrics including relayed/dropped packet counters, active connections, relay latency, FEC recoveries, auth rejections, and abuse blocks.
For the full dashboard experience, stand up the Prometheus + Grafana stack in infra/monitoring/README.md:
cd infra/monitoring
docker compose up -d
# Grafana: http://localhost:3000 (admin / lightspeed)
# Prometheus: http://localhost:9090The dashboard auto-loads with 20 panels across 6 sections (mesh overview, traffic, latency, FEC, security, sessions) and ships with 10 alert rules, including ProxyNodeDown and HighAuthRejections.
If curl http://YOUR_PROXY_IP:8080/health times out or errors:
ssh root@<vps-ip>
systemctl status lightspeed-proxy
journalctl -u lightspeed-proxy --tail 50
ss -ulnp | grep 4434Check that the firewall allows TCP 8080 and that the service is actually running.
# Which release is active, and what exists on disk
readlink -f /opt/lightspeed/current
ls -la /opt/lightspeed/current/lightspeed-proxy /opt/lightspeed/releases/
# Verify the config parses
cat /etc/lightspeed/proxy.tomlA missing or malformed config is the usual culprit. Confirm the config file exists at the path passed to --config. If a recent deploy left the node unhealthy, current will have been rolled back to the previous release automatically; check readlink -f /opt/lightspeed/current to see which version is running.
Rebuild and redeploy using the scripts in infra/scripts/:
./infra/scripts/deploy.sh # all nodes
./infra/scripts/deploy.sh relay-nrt # one nodedeploy.sh builds the binary, uploads it to a staging path on each relay, and runs relay-install.sh over SSH. Each deploy gets a unique release version (a UTC timestamp plus the short git SHA; override with LIGHTSPEED_VERSION), so the previous version is preserved:
-
relay-install.shrefuses to activate a binary that is not an executable ELF for the host arch, or whose--checkfails. - Activation is an atomic repoint of
/opt/lightspeed/current, followed by a/healthpoll of up to 15s. - On a failed health gate the installer repoints
currentback to the previous release, restarts the service, and exits non-zero, so a bad deploy leaves the node running the last known-good version. - The newest 3 releases are retained; older ones are pruned only after a healthy activation.
For a mesh-wide rollout, deploy-all.sh iterates the node inventory. To roll a node back manually, repoint the symlink and restart:
ln -sfn /opt/lightspeed/releases/<previous-version> /opt/lightspeed/current
systemctl restart lightspeed-proxyThe installer's layout/rollback logic is covered by infra/scripts/test_relay_install.sh, which runs it against a fixture root with systemctl, curl, and file stubbed out.