Using NGINX to Wrap Servatrice Websockets in SSL - Cockatrice/Cockatrice GitHub Wiki

Background

Since #3545 was merged, Servatrice now allows users to connect using websockets instead of raw TCP streams. We can take advantage of this to allow clients to leverage SSL to protect sensitive information sent to the server, such as their password.

Important Notes

  • In order to prevent ban evasion, it's recommended that you use a copy of Servatrice compiled with QT 5.6 or newer. Older versions of QT don't provide some features used by Servatrice to determine the real IP of proxied users.
  • If you want to support non-ssl websockets, it is important to not allow unproxied connections, since a user could spoof their IP by sending an X-Real-IP header.
  • Please don't use the below configs as-is. There are parts which NEED to be changed to match your specific system.

Configuring Servatrice

In order for us to accept websocket connections, we have to configure Servatrice to accept them:

[server]
; Servatrice can listen for clients on websockets, too. Unfortunately it can't support more than one thread.
; Set to 0 to disable the websocket server.
websocket_number_pools=1

; The TCP port number servatrice will listen on for websocket clients; default is 4748
websocket_port=4748

Normally, this is enough to allow a websocket connection. However, since we will be putting nginx between Servatrice and the internet, all of the IP addresses saved to the database for WS clients will be the IP of nginx instead of the IP of the users. This is bad, since it allows users to circumvent IP bans by connecting over a websocket.

To fix this and get the correct IP for each user, we have to read an HTTP header set by nginx, called X-Real-IP:

; The header to check for the client's actual IP address
web_socket_ip_header="X-Real-IP"

Configuring NGINX

Now that we have Servatrice set up to accept websocket connections, we have to tell nginx how to pass them along securely.

NOTE: This section of the guide does not cover how to set safe SSL parameters. If you are running your own server, it is assumed you already know how to do this. The configuration in this section is only as safe as your SSL settings!

NOTE: This section assumes you already have an SSL certificate. If you don't, get one now.

The upstream Directive

First, we need to tell nginx how to find your cockatrice server. This is pretty easy, since nginx provides a handy upstream directive:

upstream servatrice {
    server 127.0.0.1:4748;
}

This tells nginx that it can find Servatrice at 127.0.0.1 (localhost) on port 4748 (the default websocket port).

The server Directive

Now, we need to tell nginx where to listen for incoming connections. This is done using the server directive. If you want to use an existing server block, skip ahead to the next section.

server {
    # Our server needs to accept connections using ssl on port 443,
    # so we using the listen directive to declare that:
    listen 443 ssl;

    # Here, we specify the hostname of this server. Yours WILL be different
    server_name mtg.tetrarch.co;

    # This turns on SSL support:
    ssl on;

    # Here, we declare the locations of our SSL certificates. Yours WILL be different
    ssl_certificate         /etc/letsencrypt/live/mtg.tetrarch.co/fullchain.pem;
    ssl_certificate_key     /etc/letsencrypt/live/mtg.tetrarch.co/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/mtg.tetrarch.co/fullchain.pem;
}

Great! Restart nginx and make sure you can connect using a web browser. If you can, you're ready to move to the next section.

The location Directive

Now that you have a server block configured to accept secure connections, we need to actually forward those connections to Servatrice. To do this, we create a location inside our server block and use proxy_pass to actually forward the traffic.

An important thing to note is that when a Cockatrice client connects to a server via a websocket, it always connects to your.hostname.here:port/servatrice. Note the /servatrice at the end! This is to enable you to have a webpage on the same domain as your Servatrice, without requiring users to enter even more server info than they already do.

Since we are proxying a websocket, in addition to sending the regular websocket data, we also need to pass along a couple headers, and explicitly specify the HTTP version we use. Thankfully, nginx's proxy module provides us with easy ways to do all of that:

  • To specify the HTTP version we use to talk with Servatrice, we use the proxy_http_version directive. We choose version 1.1, since it supports keepalive connections. Currently, HTTP/2 is not supported by this module.
  • To tell Servatrice that the client wants to use a websocket connection (as opposed to a stray browser connection), we have to explicitly pass through 2 headers: Upgrade and Connection. We use the proxy_set_header directive to do this.
  • Next, we need to tell Servatrice the real IP of the client. We again use proxy_set_header, this time to set X-Real-IP to $remote_addr, a variable that contains the client's IP.
  • Lastly, we need to actually pass along the connection to Servatrice. To do this, we use proxy_pass with the name of our upstream from before.

Put together, we should have a location that looks like this:

location /servatrice {
    proxy_http_version      1.1;
    proxy_set_header        Upgrade         $http_upgrade;
    proxy_set_header        Connection      "Upgrade";
    proxy_set_header        X-Real-IP       $remote_addr;
    proxy_pass              http://servatrice;
}

The Full Config

Please don't just copy/paste this! There is important info in the above steps on how and what to change!

upstream servatrice {
    server 127.0.0.1:4748;
}
server {
    listen 443 ssl;
    server_name mtg.tetrarch.co;
    ssl on;
    ssl_certificate         /etc/letsencrypt/live/mtg.tetrarch.co/fullchain.pem;
    ssl_certificate_key     /etc/letsencrypt/live/mtg.tetrarch.co/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/mtg.tetrarch.co/fullchain.pem;
    location /servatrice {
        proxy_http_version      1.1;
        proxy_set_header        Upgrade         $http_upgrade;
        proxy_set_header        Connection      "Upgrade";
        proxy_set_header        X-Real-IP       $remote_addr;
        proxy_pass              http://servatrice;
    }
}