How can I Activate SSL for my WebGL client to work with Netcode server, while being hosted in HTTPS pages (like Itch.io)

I’m using Multiplay Server and I have multiple clients from Android, WebGL and PC. Everything works just fine, except for the WebGL client. When it is runs on an HTTPS website (A.K.A being hosted in itch.io) networking doesn’t work because I need a certificate to communicate in HTTPS, in other words, I have to use WSS.

No matter how much I searched the internet, I didn’t found a guide or best practices section or even documentation on how to handle this particular problem, so I though maybe someone in the forums had faced a similar issue before?

Have you set “wss” as the connection type when initializing the Transport? This may be all that’s required although I know you can also provide a custom certificate and key.

I’m not sure when or why to use the certificact. My guess is this is only needed for self hosted servers where you have to create your own certificate for the server machine in order to allow it to communicate over https.

1 Like

Hey! thanks for your reply.

I was trying by activating this feature directly on the UnityTransport, but it seems like it can’t handle with the direct IPs that matchmaker generated when an allocation is created. I get the following error message on my client:

WebGL.framework.js.gz:9 Exception: In order to use encrypted communications, clients must set the server common name.
at Unity.Netcode.Transports.UTP.UnityTransport.CreateDriver (Unity.Netcode.Transports.UTP.UnityTransport transport, Unity.Networking.Transport.NetworkDriver& driver, Unity.Networking.Transport.NetworkPipeline& unreliableFragmentedPipeline, Unity.Networking.Transport.NetworkPipeline& unreliableSequencedFragmentedPipeline, Unity.Networking.Transport.NetworkPipeline& reliableSequencedPipeline) [0x00000] in <00000000000000000000000000000000>:0

You might find the information in this thread interesting. The short version is that the process will depend on where the server is hosted. If it’s a server you control on your own domain name, you “just” need to obtain certificates for it. If it’s on Multiplay you’ll unfortunately need to use either a reverse proxy or Unity Relay. And for third-party providers it will depend on each one (I think some provide ready-made certificates).

1 Like

I’m not sure if I can use the relays with multiplay and matchmaking, but I can certainly do the proxy. Thanks for the guidance!

After trying to implement this for a couple of days, I’m having issues implementing the reverse proxy for my dedicated server. It seems that I need it to be outside of the Unity services in order for it to be HTTPS, so docker is out of the question. Coul dyou provide me some guides on how to create the reverse proxy? My server is hosted in multiplay, and since it’s matchmaker that is creating the allocations, it’s hard to know how to setup a proxy that properly directs to the right server.

The following guide is for Mirror, but it covers the basics pretty well. The big challenge with Multiplay though is that the IP address of the server is assigned dynamically. That can be problematic since you’ll need a way to dynamically reconfigure the reverse proxy when a server is spun up.

Unfortunately that is not a simple task. Which is why I think most users in your situation ended up going with Unity Relay. I’m not a services expect, but if you’re using the Multiplayer Services SDK it should be possible to use it even if you are using Matchmaker. This thread has some more information about this.

1 Like

I think the only thing that scares me off of using Relay is that I’m building a competitive game that needs to be server authoritative, and I don’t know if relays can work with dedicated servers, or if it has to be a host specifically that’s client and servert, that would be a nono. I’ll perform my research on relay since it’s easy to implement though

I understand why you’d be wary in your situation, but ultimately the Relay service is just a way to move packets from one peer to another. There’s no requirement that both peers be “players” in the game (e.g. with one acting both as a client and server). It’s fine to have a server that just acts as a server and clients that are just clients.

All of a sudden that sounds alot more appealing. If I can have relay with dedicated multiplay servers, O’
ll do that! I needrelay down the line in any case to do custom games. Thganks simon!

I got it working, it was way more work than I anticipated. the way everyone goes around thi sproblem by using relay and lobbies is very mediocre, it’s basically a series of workarounds that require a lot of network bandwidth, creates race conditions also, in summary, it’s a mess. I hope you guys manage to get the multiplay sorted out because using tools like lobby to get around sharing the relay code forces developer to build a terrible network architecture, not scalable at all. Basically, if you think of the architecture of this thing, it’s using relay as a reverse proxy and lobby as an improvised messaging queue. WebGL is a very prominent format for many things, and let’s not set aside the amount of technical debt this adds to the users of Unity. There is a ridiculous amount of posts about this very subject to treat this as a nice to have.

To anyone in the future having htis issue, this is how you setup the server in Multiplay to use relay and WSS:

protected async Task<string> InitializeRelayServer()
    {
        await UnityServices.InitializeAsync();
        await AuthenticationService.Instance.SignInAnonymouslyAsync();

        Allocation allocation = await RelayService.Instance.CreateAllocationAsync(3);
        string joinCode = await RelayService.Instance.GetJoinCodeAsync(allocation.AllocationId);

        Debug.Log($"Relay server started with join code: {joinCode}");
        //If you use Relay SDK instead of Multiplayer, this method changes, there are many examples in the internet of that specific case.
        var relayServerData = AllocationUtils.ToRelayServerData(allocation, "wss");

        NetworkManager.Singleton.GetComponent<UnityTransport>().SetRelayServerData(relayServerData);
        NetworkManager.Singleton.StartServer();
        return joinCode;
    }

...
            string relayJoinCode = await InitializeRelayServer();
            var lobbyData = new Dictionary<string, DataObject>();
            lobbyData.Add("relayCode", new DataObject(
                visibility: DataObject.VisibilityOptions.Public,
                value: relayJoinCode,
                //Store it in a free string slot in your lobby
                index: DataObject.IndexOptions.S3));
            var serverConfig = MultiplayService.Instance.ServerConfig;
            try
            {
                //treat your multiplayer data, decide on game modes, or do whatever you do with it
                    ...
                    CreateLobbyOptions createLobbyOptions = new CreateLobbyOptions();
                    createLobbyOptions.Data = lobbyData;
                    var matchmakingTicket = JsonConvert.DeserializeObject<MatchData>(payloadJson);
                    lobby = await LobbyService.Instance.CreateLobbyAsync(matchmakingTicket.MatchId, 3, createLobbyOptions);
                serverQueryHandler = await MultiplayService.Instance.StartServerQueryHandlerAsync(2, "WarringTriad", "Random", "", "");
                NetworkManager.Singleton.OnClientDisconnectCallback += OnClientDisconnect;
                NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnect;

                if (serverConfig.AllocationId != "")
                {
                    Debug.Log("Server Started!");
                }
            }

            await MultiplayService.Instance.ReadyServerForPlayersAsync();

On the client side, you need a retry method to get around the race condition of clients tryuign to access the lobby before it has been created:

//You call this method on the update to receive updates from the matchmaking. I won't go into detail of how to use matchmaking, there are other tutos for that
public async void PollMatchmakerTicket()
    {
        TicketStatusResponse ticketStatusResponse =
            await MatchmakerService.Instance.GetTicketAsync(createTicketResponse.Id);

        if (ticketStatusResponse != null)
        {
            if (ticketStatusResponse.Type == typeof(MultiplayAssignment))
            {
                MultiplayAssignment multiplayAssignment = ticketStatusResponse.Value as MultiplayAssignment;

                switch (multiplayAssignment.Status)
                {
                    case MultiplayAssignment.StatusOptions.Found:
                    {
                        Debug.Log("Found!");
                        createTicketResponse = null;
                        QueryForServerLobby(multiplayAssignment.MatchId);
                        break;
                    }
                    case MultiplayAssignment.StatusOptions.InProgress:
                        break;
                    case MultiplayAssignment.StatusOptions.Failed:
                    {
                        Debug.Log("Failed!");
                        createTicketResponse = null;
                        waitingForOpponetGameObject.SetActive(false);
                        break;
                    }
                    case MultiplayAssignment.StatusOptions.Timeout:
                    {
                        Debug.Log("Timed out...");
                        createTicketResponse = null;
                        waitingForOpponetGameObject.SetActive(false);
                        break;
                    }
                }
            }
        }
    }

    private void QueryForServerLobby(string matchId)
    {
        //This is my retry mechanism, the loading screen shows a spinner and then retries an awaitable method with a delay of 2 seconds between each attempt, receiving the matchId as parameter for the awaitable method
        StartCoroutine(loadingScreenController.ExecuteRetryableActionWithData(ConnectToServerLobby, (response) => { }, new List<string>{matchId}));
    }

    //Lobby querying, startng relay client and connecting to relay server.
    private async Task<bool> ConnectToServerLobby(string matchId)
    {
        QueryLobbiesOptions options = new QueryLobbiesOptions();
        options.Count = 1;

        // Filter for lobby with matching Matchmaker ticket
        options.Filters = new List<QueryFilter>()
        {
            new(
                field: QueryFilter.FieldOptions.Name,
                op: QueryFilter.OpOptions.EQ,
                value: matchId)
        };

        QueryResponse lobbies = await LobbyService.Instance.QueryLobbiesAsync(options);
        string lobbyId = lobbies.Results.First().Id;

        Lobby lobby = await LobbyService.Instance.JoinLobbyByIdAsync(lobbyId);
        string relayCode = lobby.Data["relayCode"].Value;
        await JoinRelayServer(relayCode);
        return true;
    }

private async Task JoinRelayServer(string joinCode)
    {
        JoinAllocation joinAllocation = await RelayService.Instance.JoinAllocationAsync(joinCode);

        // Configure the transport with relay data
        var transport = NetworkManager.Singleton.GetComponent<UnityTransport>();
        transport.SetRelayServerData(AllocationUtils.ToRelayServerData(joinAllocation, "wss"));

        NetworkManager.Singleton.StartClient();
        Debug.Log("Client connected to Relay Server.");
    }