> For the complete documentation index, see [llms.txt](https://docs.edgegap.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.edgegap.com/docs/sample-projects/unity-netcodes/small-network-relay.md).

# Lightnet Small Network Relay

Host a small extensible network relay on Edgegap and play Tanks through it on Mirror, FishNet, PurrNet or Netcode for Game Objects, with room codes or Edgegap matchmaking.

Explore how to run your own small network relay on Edgegap, with a Unity game sample that plays through it using [Mirror](https://mirror-networking.com/), [FishNet](https://fish-networking.gitbook.io/docs), [PurrNet](https://purrnet.dev/) or [Netcode for GameObjects (NGO)](https://docs-multiplayer.unity3d.com/netcode/current/about/).

The relay is a small .NET server built on [LightNet](https://github.com/edgegap/server-sample-unity-smart-relays). Edgegap hosts and orchestrates it like any other server: one deployment, started near your players. One player hosts a room and gets a 6-letter code, the other players join with that code. No player opens a port: everyone, including the host, is a client of the relay.

This sample shows:

* **a relay server** in a `linux/amd64` Docker image, listening on port `7777` for both TCP (reliable messages) and UDP (unreliable messages),
* **rooms with a join code** - many rooms can share one relay deployment,
* **the Tanks sample on four netcodes** - Mirror, FishNet, PurrNet and NGO in one Unity project, one build,
* **one setting to switch netcode** - in a settings asset, an editor menu, in the game before connecting, or on the command line,
* **optional Edgegap matchmaking** - off by default: **Find Match** puts nearby players together on a relay near them, one player per room hosts it, players who search later join running rooms, and a relay with full rooms opens new ones before another relay starts,
* **optional encrypted connection** - `wss://` through Edgegap's TLS Upgrade, next to plain TCP and UDP,
* **automated tests** - game clients play through the relay on each netcode, with room codes or through the matchmaker.

{% hint style="success" %}
**This is a client-host setup**: the hosting player's runs as server and relay forwards bytes between players.
{% endhint %}

{% hint style="info" %}
Alternative - [dedicated servers](/learn/orchestration/deployments.md#orchestration) prevent host player advantage and the match survives the host leaving.
{% endhint %}

## ✔️ Preparation

Before we start, you will need:

* [Small Network Relay sample project](https://github.com/edgegap/server-sample-lightnet-relay),
* [Unity `6000.0.54f1`](https://unity.com/releases/editor/archive), to open and build the game,
* [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) (optional), only to run the relay outside Docker.

<details>

<summary><a href="https://www.docker.com/products/docker-desktop/">Install Docker Desktop (or Docker CLI)</a></summary>

* [Install Docker Desktop from the official source](https://www.docker.com/products/docker-desktop/) (no account required).
* Restart your computer after completing the installation.

</details>

The repository has four parts:

| Folder                   | What it is                                                                                            |
| ------------------------ | ----------------------------------------------------------------------------------------------------- |
| `LightNet`               | LightNet networking core as a Unity package, plus the relay protocol and its netcode-agnostic client. |
| `relay-server`           | Relay Server - .NET 8 console app built from the same sources, and its Dockerfile.                    |
| `tanks`                  | the Unity project: Mirror's Tanks example on Mirror, FishNet, PurrNet and NGO.                        |
| `matchmaker-config.json` | optional matchmaker configuration, see [Optional Matchmaking](#optional-matchmaking).                 |

## 🔀 Pick the Netcode

The netcode is the `netcode` field of `tanks/Assets/Tanks/Resources/TanksSettings.asset`: `Mirror`, `FishNet`, `PurrNet` or `NGO`. The same asset holds the relay address and port.

You can also switch it:

* in the Unity Editor, with the **Tanks > Netcode** menu - this also opens that netcode's scene,
* in the game, with the netcode buttons at the top of the screen, before hosting or joining,
* on the command line, with `-netcode FishNet`.

| Netcode | Version | Tank sync                                                                                |
| ------- | ------- | ---------------------------------------------------------------------------------------- |
| Mirror  | 66.0.9  | `SyncVar`, `Command`, `ClientRpc`, client authoritative `NetworkTransform`               |
| FishNet | 4.7.4   | `SyncVar<int>`, `ServerRpc`, `ObserversRpc`, client authoritative `NetworkTransform`     |
| PurrNet | 1.24.1  | `SyncVar<int>`, `ServerRpc`, `ObserversRpc`, owner authoritative `NetworkTransform`      |
| NGO     | 1.13.0  | `NetworkVariable<int>`, `ServerRpc`, `ClientRpc`, owner authoritative `NetworkTransform` |

{% hint style="warning" %}
**The host and every player must use the same netcode.** The relay doesn't verify netcode compatibility.
{% endhint %}

Each netcode has its own relay transport, built on the same relay client (`RelayPeer`): `MirrorRelayTransport`, `FishNetRelayTransport`, `PurrNetRelayTransport` and `NgoRelayTransport`. To use the relay in your own game, copy the `LightNet` package and the transport for your netcode.

## ⚡ Deploy and Connect

### 1. Run the Relay Locally

☑️ From the repository root, build and run the relay image:

{% code collapsedlinecount="10" %}

```bash
docker build --platform linux/amd64 -f relay-server/Dockerfile -t lightnet-relay:dev .
docker run -p 7777:7777/tcp -p 7777:7777/udp -p 7778:7778/tcp lightnet-relay:dev
```

{% endcode %}

☑️ Open `tanks` in Unity, pick a netcode, and build with **Tanks > Build Windows client**, or press Play on `Assets/Tanks/Scenes/Boot.unity`.

☑️ In the first game, press **Host**: the room code appears on screen. In the second game, type the code and press **Join room**.

✅ You may now proceed to the next step.

### 2. Push the Relay Image

☑️ To get started, you'll need to [create a free account with Edgegap](https://app.edgegap.com/auth/register). No credit card required.

☑️ Find your [Container Registry](/learn/advanced-features/edgegap-container-registry.md) project name and credentials.

☑️ Sign in to Edgegap's Container Registry with Docker, using your registry project name, username and token:

{% tabs %}
{% tab title="Bash (Linux)" %}

```bash
project='<REGISTRY_PROJECT>';
username='robot$<REGISTRY_ORGANIZATION_ID>+client-push';
token='<REGISTRY_TOKEN>';

echo "$token" | docker login registry.edgegap.com -u "$username" --password-stdin 2>&1
```

{% endtab %}

{% tab title="PowerShell (Windows)" %}

```powershell
$project = '<REGISTRY_PROJECT>'
$username = 'robot$<REGISTRY_ORGANIZATION_ID>+client-push'
$token = '<REGISTRY_TOKEN>'

echo "$token" | docker login registry.edgegap.com -u "$username" --password-stdin 2>&1
```

{% endtab %}
{% endtabs %}

☑️ Tag and push the relay image, with a new tag for every build:

{% code collapsedlinecount="10" %}

```bash
docker tag lightnet-relay:dev registry.edgegap.com/<project>/lightnet-relay:<tag>
docker push registry.edgegap.com/<project>/lightnet-relay:<tag>
```

{% endcode %}

{% hint style="danger" %}
:x: **DO NOT - overwrite existing tags or use `latest` tag** - this will deploy an outdated cache.\
:white\_check\_mark: **DO - always increase your version tag** and deploy the new build.
{% endhint %}

{% hint style="warning" %}
Each organization gets their own registry project, **make sure to modify the commands to match yours.**
{% endhint %}

✅ You may now proceed to the next step.

### 3. Create an App Version

☑️ [Create an application](https://app.edgegap.com/application-management/applications/list) named `lightnet-relay`, and a version with:

* image `registry.edgegap.com/<project>/lightnet-relay` and your tag,
* your Container Registry username and token, so Edgegap can pull your image,
* resources 0.25 vCPU and 512 MB memory,
* one [port](/learn/orchestration/application-and-versions.md) `7777`, named `relay`, protocol **TCP/UDP**,
* optional, for the [encrypted connection](#encrypted-connection): a second port `7778`, named `relay-ws`, protocol **WS**, with **TLS Upgrade** on,
* [max duration](/learn/orchestration/application-and-versions.md) 30 minutes for testing.

{% hint style="warning" %}
Use one port entry with protocol **TCP/UDP**, not separate `7777` TCP and `7777` UDP entries. Edgegap then gives TCP and UDP the same external port, so players only need one port number.
{% endhint %}

Optional relay settings, as [environment variables on the app version](/learn/orchestration/application-and-versions.md#injected-variables):

| Variable                  | Default | What it does                                                                                                                    |
| ------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `RELAY_TCP_PORT`          | `7777`  | port for reliable messages                                                                                                      |
| `RELAY_UDP_PORT`          | `7777`  | port for unreliable messages                                                                                                    |
| `RELAY_MAX_PEERS`         | `256`   | maximum concurrent connections, across all rooms                                                                                |
| `RELAY_IDLE_EXIT_SECONDS` | `600`   | stop the relay when no room exists for this long, `0` = never (relays started by the matchmaker stop when their last room ends) |
| `RELAY_WS_PORT`           | `7778`  | port for the WebSocket connection, `0` = off                                                                                    |
| `RELAY_MATCH_MAX_PLAYERS` | `4`     | players per room on relays started by the matchmaker; backfill fills up to this                                                 |
| `RELAY_MAX_ROOMS`         | `4`     | rooms per relay started by the matchmaker                                                                                       |
| `RELAY_SOLO_ROOM_SECONDS` | `120`   | close a room whose host has been alone this long; the game then searches again                                                  |
| `MATCHMAKER_URL`          |         | matchmaker API URL, for [backfill](#optional-matchmaking)                                                                       |
| `MATCHMAKER_TOKEN`        |         | matchmaker Auth Token, for backfill                                                                                             |

✅ You may now proceed to the next step.

### 4. Deploy and Play

☑️ [Deploy your new app version](https://app.edgegap.com/deployment-management/deployments/create) near your players. When it is ready, note its host name (`<id>.pr.edgegap.net`) and the external port of `relay`.

☑️ In both games, **set the relay address and port on screen before connecting**, or start them from the command line (alternative):

{% code collapsedlinecount="10" %}

```bash
Tanks.exe -netcode NGO -relay <id>.pr.edgegap.net -relayPort <external port> -host
Tanks.exe -netcode NGO -relay <id>.pr.edgegap.net -relayPort <external port> -join <code>
```

{% endcode %}

☑️ Host a room in the first game and join it with the code from the second.

{% hint style="info" %}
Relay log shows a player that connects and leaves right after the deployment starts: that is Edgegap's readiness check, not a player.
{% endhint %}

<figure><img src="https://3027732442-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsR0dHSFv9ymoC0DO5G8J%2Fuploads%2Fz9xjzvZwKSIp9IeC9qo8%2Fimage.png?alt=media&amp;token=e3f345ac-848d-4469-b66f-3655cd393cf3" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
The external port of your Deployment on Edgegap cloud will be chosen at random, so that a potential attacker (hacker) is slowed down and detected before they can cause damage.
{% endhint %}

{% hint style="warning" %}
**Disable VPN when testing** for more realistic conditions and receive a [low-latency deployment](/learn/orchestration/deployments.md#server-placement).
{% endhint %}

☑️ Once you verify you’re able to connect to your Deployment without issues and are done testing, **Stop your Deployment** to free up capacity in your account for the next build.

* In case you encounter issues, [inspect Dashboard logs of your deployment](https://app.edgegap.com/deployment-management/deployments/list).
* If you can’t figure out the issue, we’re hanging out in our [Community Discord](https://discord.gg/NgCnkHbsGp) and happy to help.

🙌 Congratulations on your first Deployment on Edgegap! If you’d like to learn more, keep reading.

## 🎯 Optional Matchmaking

Room codes work among friends. To match players who don't know each other, turn on Edgegap [Matchmaking](/learn/matchmaking.md): players press **Find Match**, the matchmaker groups 2 to 4 players near each other and starts a relay for their match, near them. The relay then picks one player to host and gives the others the room code, so nobody types a code. Players who press **Find Match** later join rooms with free seats, and a relay whose rooms are full opens new ones, before the matchmaker starts another relay. It's off by default, and hosting and joining by code keep working next to it.

How a match starts:

1. Each player's game measures its latency to Edgegap's beacons and creates a matchmaking ticket.
2. The matchmaker groups the tickets and deploys a relay from your app version. Edgegap gives that relay the list of matched tickets (`MM_TICKET_IDS`).
3. Each game connects to the relay with its ticket ID. The first matched player to ask hosts the match room, the others join it. Tickets that are not in the match are refused.
4. While a room has free seats, the relay asks the matchmaker for more players with backfills. Once every room is full, it offers a new room instead. See step 3 below.
5. When the last room ends, the relay deletes its backfills and stops its own deployment, so an ended match costs nothing.

A relay deployed without a matchmaker works as before, with room codes.

### 1. Create a Matchmaker

☑️ Open `matchmaker-config.json` from the repository and set `application.name` and `version` to your app version from step 3:

{% code title="matchmaker-config.json" lineNumbers="true" expandable="true" collapsedlinecount="10" %}

```json
{
  "version": "3.3.3",
  "inspect": true,
  "max_deployment_retry_count": 3,
  "profiles": {
    "casual": {
      "ticket_expiration_period": "30s",
      "ticket_removal_period": "1m",
      "group_inactivity_removal_period": "5m",
      "application": {
        "name": "lightnet-relay",
        "version": "<your version>"
      },
      "rules": {
        "initial": {
          "match_size": {
            "type": "player_count",
            "attributes": {
              "team_count": 1,
              "min_team_size": 9999,
              "max_team_size": 9999
            }
          },
          "beacons": {
            "type": "latencies",
            "attributes": {
              "difference": 100,
              "max_latency": 150
            }
          }
        },
        "expansions": {
          "10": {
            "match_size": {
              "team_count": 1,
              "min_team_size": 2,
              "max_team_size": 4
            }
          }
        }
      }
    }
  }
}
```

{% endcode %}

For its first 10 seconds a ticket can only join a relay that has room (`min_team_size` `9999` can't be met). After 10 seconds the usual rule applies, and 2 to 4 waiting players get a new relay.

The `beacons` rule keeps players with players near them: a beacon counts if the player's latency to it is under 150 ms, and matched players' latencies to it must be within 100 ms of each other. The game sends its latencies in every ticket (**Matchmaker Latency Beacons** in `TanksSettings.asset`, on by default).

{% hint style="warning" %}
**Browsers can't ping beacons.** For a WebGL build, use a profile without the `beacons` rule and turn **Matchmaker Latency Beacons** off.
{% endhint %}

☑️ In the dashboard, open [Matchmaker](https://app.edgegap.com/matchmaker-management-v2/matchmakers/list), click **Create Matchmaker**, upload the file, then **Create and Start**.

☑️ When the matchmaker is **Online**, copy its **API URL** and **Auth Token**.

{% hint style="info" %}
The Auth Token is safe to ship in your game: it lets players create tickets, and gives no access to your Edgegap account.
{% endhint %}

☑️ Add them to your app version as the environment variables `MATCHMAKER_URL` and `MATCHMAKER_TOKEN`, so relays can fill free seats (step 3).

✅ You may now proceed to the next step.

### 2. Find a Match

☑️ In `tanks/Assets/Tanks/Resources/TanksSettings.asset`, turn on **Use Matchmaker** and paste the **Matchmaker Url** and **Matchmaker Token**. The profile (`casual`) and port name (`relay`) already match the configuration above.

☑️ Build the game, start two copies and press **Find Match** in both. Or start them from the command line:

{% code collapsedlinecount="10" %}

```bash
Tanks.exe -netcode NGO -matchmaker https://<id>.edgegap.net -matchmakerToken <token> -findMatch
```

{% endcode %}

☑️ Both games show the match status, then one hosts and the other joins: both tanks appear in both games.

{% hint style="warning" %}
**Test with two players.** A match needs at least 2 players, so a single ticket keeps searching and expires after 30 seconds.
{% endhint %}

The matchmaking client is `Assets/Tanks/Core/TanksMatchmaker.cs`, built on the [Edgegap Unity SDK](https://github.com/edgegap/sdk-unity-sessions-matchmaker-server-browser). It never depends on the netcode: it puts the relay address in the settings and starts the game as host or as player, like the **Host** and **Join room** buttons do.

✅ You may now proceed to the next step.

### 3. One Relay, Several Rooms

A relay started by the matchmaker keeps working for it after its first match starts, through backfills. A backfill is a ticket owned by the relay that carries its own address. When the matchmaker puts a player's ticket in one, that player gets the relay's address, asks for its role like the others and joins or hosts a room. The game needs nothing extra: it's the same **Find Match** flow.

* **Rooms with free seats come first.** While a room has fewer than `RELAY_MATCH_MAX_PLAYERS` (default `4`) players, in the room or on their way, the relay keeps up to 3 backfills open for it. A player placed there joins that room.
* **Then new rooms.** Once every room is full and the relay has fewer than `RELAY_MAX_ROOMS` (default `4`) rooms, it offers one new room. The player placed there hosts it, and the room fills like the others.
* **Only nearby players.** The relay sends the room's own players' tickets with its backfills, so the `beacons` rule checks a new player against them.
* **A host left alone** for `RELAY_SOLO_ROOM_SECONDS` (default `120`) is told the room closed, and the game searches for another match by itself.
* **The relay stops when its last room ends**, not when one host leaves. It deletes its backfills first, so no player is sent to a relay that is gone.
* **Rate limits.** At most 5 backfills are open per relay, created at least 0.3 seconds apart.

☑️ With a match running between two games, press **Find Match** in a third: it joins the running room, and all three games see three tanks.

☑️ Fill a room with 4 games, then press **Find Match** in two more: the first hosts a new room on the same relay, and the second joins it.

{% hint style="info" %}
Without `MATCHMAKER_URL` and `MATCHMAKER_TOKEN` on the app version, the relay logs `backfill: off`: its rooms don't take new players and it opens no new rooms.
{% endhint %}

The relay's backfill code is in `relay-server/MatchBackfill.cs` and `relay-server/RelayServer.cs`.

## 🔒 Encrypted Connection

By default, game traffic through the relay is **not encrypted**: it goes over plain TCP and UDP. What protects players is that nobody sees another player's IP address, and with the matchmaker only matched players get the room code. Calls to the matchmaker use HTTPS.

For encryption, players can connect over a WebSocket instead. With **TLS Upgrade** on the `relay-ws` port (step 3), Edgegap holds the certificate and the connection is `wss://`: traffic between each player and Edgegap is encrypted, and the relay needs no certificate.

☑️ Give the game a `wss://` relay address instead of a host name, with the external port of `relay-ws`: on screen, in `TanksSettings.asset`, or on the command line:

{% code collapsedlinecount="10" %}

```bash
Tanks.exe -netcode NGO -relay wss://<id>.pr.edgegap.net:<relay-ws external port> -host
```

{% endcode %}

☑️ With the matchmaker, turn on **Matchmaker Use Web Socket** in `TanksSettings.asset` (or `-matchmakerWebSocket`): the game connects to the match relay's `relay-ws` port.

{% hint style="warning" %}
**WebSocket runs over TCP only**: "unreliable" messages arrive reliably and in order, which adds delay on a poor connection. It suits co-op and turn-based games better than fast shooters, so it's an option next to TCP and UDP. Players on TCP and UDP and players on WebSocket can share a room.
{% endhint %}

{% hint style="info" %}
Connect with the host name (`<id>.pr.edgegap.net`), not the IP address: the certificate is for the host name. The encryption stops at Edgegap: the relay, and the host's game, still see what they forward, as with any relay.
{% endhint %}

## 🧪 Automated Test

With a Windows build and a running relay (local or on Edgegap), `e2e.sh` starts a host and a client on each netcode:

{% code collapsedlinecount="10" %}

```bash
./e2e.sh <id>.pr.edgegap.net <external port>
./e2e.sh 127.0.0.1 7777 FishNet NGO
./e2e.sh wss://<id>.pr.edgegap.net:<relay-ws external port> 0
```

{% endcode %}

The client drives and fires. A netcode passes when both games see two tanks, both received data on the reliable and unreliable channels, both saw a projectile spawn, and the host saw the client's tank move.

With a matchmaker, `e2e-matchmaker.sh` runs the same test through it: on each netcode, two games press **Find Match** and play the match on the relay the matchmaker started. With `BACKFILL=1`, a third game presses **Find Match** once the match runs and must join it through backfill. With `MULTIROOM=1`, four games fill a room, then a fifth must host a new room on the same relay and a sixth must join it. With `MM_WEBSOCKET=1`, the games connect over `wss://`.

{% code collapsedlinecount="10" %}

```bash
./e2e-matchmaker.sh https://<id>.edgegap.net <token>
./e2e-matchmaker.sh https://<id>.edgegap.net <token> FishNet NGO
BACKFILL=1 ./e2e-matchmaker.sh https://<id>.edgegap.net <token>
MM_WEBSOCKET=1 BACKFILL=1 ./e2e-matchmaker.sh https://<id>.edgegap.net <token>
MULTIROOM=1 ./e2e-matchmaker.sh https://<id>.edgegap.net <token>
```

{% endcode %}

## 💡 Good to Know

* **How the relay forwards data** - the host's messages go to one player, and each player's messages go only to the host. Players never talk to each other directly. Reliable messages go over TCP and unreliable ones over UDP, or both over one WebSocket.
* **Message size** - up to 1,177 bytes per unreliable message and 16 KB per reliable message. The transports report these limits to their netcode.
* **When the host leaves** - the room closes and the relay disconnects its players. The netcodes in this sample don't migrate the host.
* **One relay or many** - one deployment serves many rooms when players use room codes. With the [matchmaker](#optional-matchmaking), each relay hosts up to `RELAY_MAX_ROOMS` rooms near its players, and a new relay starts only when nearby relays are full.
* **Changes to LightNet** - the sample's LightNet works behind NAT and port mapping: the relay answers each player's UDP traffic at the address it came from. See the repository's README for the full list.
