This is just a quick note on exposing a game server securely over proxied connection. I was looking for a solution similar to Cloudflare DDoS Protection, specifically obfuscating real server IP (not much the DDoS part), but for game servers and not just websites. Free tier from Cloudflare is limited to HTTP (80) and HTTPS (443) traffic only. Although there are some games that could be hosted with just one or two of these ports, it’s against Cloudflare’s ToS. I wouldn’t risk account termination to find out how soon they can detect it.

Instead, I found rathole, which allows us to forward traffic from a publicly available server to a client machine on the local network, behind NAT. I’m using it in conjunction with a free tier E2 instance (t2.micro) from AWS.

How does it work?

Here’s a crude diagram for you, watch the direction of arrows:

PlayerInternetPublicserverGameseLrAvNerathome

We’re initiating the connection to the public server (running rathole in server mode) from our game server (running rathole in client mode). The game server is in our home network behind NAT. That connection is then used in reverse for all players who also connect to the public server over Internet, and expect to reach our game server. That way our game server stays private, and it is the public server that gets exposed on the Internet. No holes (open ports) in your home firewall are required, and no need to expose your home IP address. Being cautious, I would still put the game server on a separated VLAN within local network at home, and treat it as publicly exposed.

This setup will increase latency for the players (additional 20-40ms from my experience) for the benefit of added security. I think it’s well worth it for non-competitive games.

You need a publicly available server of course for all this to work.

Free cloud server, anyone?

The public server in this case can be a very inexpensive instance with minuscule resources, as the only thing it essentially does is act as a proxy, serving traffic between both ends.

Amazon Web Services offers E2 instances with 1 vCPU, 1 GB of memory, and up to 30 GB of SSD storage for free, for the period of one year. It’s not the only provider offering free tier for compute resources. Google and Microsoft have similar options. Most other providers will impose 30 days limit on you, but these three giants extend it for a full year. If you are willing to migrate once every rotation around the sun, you could definitely run like this for a while. It doesn’t take much time to setup anyway 😄

Probably the best option at the time of writing this is offered by Oracle, with AMD-based x86 compute resources that are free forever (or until they change their terms). No time limit. If you are willing to go ARM route, you can also get Ampere A1 based instances with 24 GB of memory running 24/7/365 for no cost to you. Sounds incredible, right? The only caveat… it’s Oracle. I wasn’t able to pass payment card verification when creating an account (which is still required, even if you intend to use free tier services). Judging from multiple reports online, I’m not the only one, and the process appears to be broken for certain addresses and geographic locations, by false positively flagging users as fraudulent. I tried contacting Oracle support couple weeks ago, but I haven’t received any response, not counting automated reply with my case number. If you are lucky enough to pass the account creation stage, well, enjoy! Make sure to take regular backups though, as other people online report disappearing accounts and instances, with no option to recover data and no clear information from Oracle as to what was the reason for account removal. Knowing how difficult it is to reach Oracle support, I’d be little wary of that. Unsurprisingly, non-paying customers are not a priority.

What’s the caveat?

Egress traffic. Most cloud providers have a strict limit on the amount of data transferred from the instance, before they start charging for additional gigabytes. This is still fine if you are planning to host a server for a handful of friends, and play online couple days a week, but anything more intensive will likely go over free tier limit.

Monitoring this also isn’t as straightforward as it should be on most platforms. Perhaps on purpose? For AWS, you can keep an eye on MTD usage within Free tier section on Billing page. Make sure to also setup alerts for getting close to free tier limits. They are opt-in, so if you don’t take action on this now you might get an unexpected bill later.

There’s also a detailed network usage report that can be generated from AWS CLI, following this article.

Free tier from AWS includes 100GB of outbound data per month.

Let’s set it up

rathole is available for Windows, macOS and Linux. Here I’m setting it up on Ubuntu Server 22.04 LTS, both the client (on the game server behind NAT) and server part (on the public server in the cloud). Going forward I’ll refer to them as such:

Client = Game server behind NAT

Server = Public server on the Internet

Info

For security reasons, I chose to run rathole under a dedicated service account. Configuration explained below will not work with network ports below 1024. These are known as privileged ports. I do not know of any games that require them. If you want to expose services on privileged ports, you’ll need to use a different approach for running rathole with elevated permissions or as root.

Initial steps, for both the client and the server

First, download latest release of rathole and extract the archive. At the time of writing this it’s v0.4.8. My preference is to save it in /usr/local/bin/. Make sure to install unzip if it’s not already present on your system.

wget https://github.com/rapiz1/rathole/releases/download/v0.4.8/rathole-mipsel-unknown-linux-gnu.zip && sudo unzip rathole-mipsel-unknown-linux-gnu.zip -d /usr/local/bin/

Add rathole user:

sudo adduser --system --home /opt/rathole --shell /bin/false --disabled-login --disabled-password --group rathole

Add yourself to rathole group:

sudo usermod -a -G rathole $USER

Change permissions on rathole home directory, so group members can access it:

sudo chmod -R 771 /opt/rathole

Generate secret token, which will be used for authentication between the client and the server later. This can be any string of characters. Here’s a quick way to generate random string and save it as secret_token.txt file in current working directory:

tr -dc A-Za-z0-9 </dev/urandom | head -c 64 > secret_token.txt

Server side (public server)

Let’s get an example of rathole’s server side config file:

wget https://raw.githubusercontent.com/rapiz1/rathole/main/examples/minimal/server.toml

You’ll see the config file is pretty simple:

[server]
bind_addr = "0.0.0.0:2333"
default_token = "123"

[server.services.foo1]
bind_addr = "0.0.0.0:5202"

In [server] section we need to change default_token to the secret string of characters we generated earlier. This must be set to the same value on both the server and the client. If you leave bind_addr as is (0.0.0.0), rathole will listen on all interfaces (usually that’s what you want). Take note of the port number (0.0.0.0:2333) and change it if you like, as that port needs to be open on the public server. This will be used by rathole itself to establish its own management connection between the client and the server.

In [server.services.foo1] we have more things to set. First, we can change the name after the last dot to be more descriptive of the actual game we want to host. This will show up later in the logs. Bind address follows the same logic as before, however port number depends on the specific game or service you want to host. I’ll use Satisfactory in this example. It requires UDP ports 15777, 15000 and 7777. Online documentation for your game should list ports that are required. We need separate entry for each port, so my server config for rathole looks like this:

# server.toml
[server]
bind_addr = "0.0.0.0:2333"
default_token = "SuperSecretUnguessableTokenThatRequiresQuantumComputerToBeBrokenNotReallyButHey"

[server.services.satisfactory-01]
bind_addr = "0.0.0.0:15777"
type = "udp"

[server.services.satisfactory-02]
bind_addr = "0.0.0.0:15000"
type = "udp"

[server.services.satisfactory-03]
bind_addr = "0.0.0.0:7777"
type = "udp"

Tip

If you omit type = "udp", TCP is used by default.

I’m going to store my config files in /opt/rathole/, and name them accordingly to what is hosted. In this case, that’s satisfactory.toml. This will be important in a minute for starting and stopping systemd service.

Change ownership on the config file:

sudo chown rathole:rathole /opt/rathole/satisfactory.toml

Change permissions on the config file, so only rathole user can access it. If you later want to edit it, use sudo:

sudo chmod 600 /opt/rathole/satisfactory.toml

Now create systemd service file as /etc/systemd/system/[email protected]:

[Unit]
Description=Rathole Server Service
After=network.target

[Service]
User=rathole
Group=rathole
Type=simple
Restart=on-failure
RestartSec=5s
LimitNOFILE=1048576

ExecStart=/usr/local/bin/rathole -s /opt/rathole/%i.toml

[Install]
WantedBy=multi-user.target

Reload daemons:

sudo systemctl daemon-reload

Start the service. Note that after @ symbol I’m calling the name of the config file I created earlier, satisfactory.toml:

sudo systemctl start rathole-server@satisfactory

This allows you to start multiple instances of rathole, using separate config files for hosting different services, and controlling them independently.

Tip

You can start the service now and enable it to start automatically on every boot with:

sudo systemctl enable rathole-server@satisfactory --now

Check if the service was started successfully:

sudo systemctl status rathole-server@satisfactory
[email protected] - Rathole Server Service
     Loaded: loaded (/etc/systemd/system/[email protected]; disabled; vendor preset: enabled)
     Active: active (running) since Fri 2023-09-01 17:56:48 UTC; 5s ago
   Main PID: 44729 (rathole)
      Tasks: 4 (limit: 1121)
     Memory: 9.2M
        CPU: 177ms
     CGroup: /system.slice/system-rathole\x2dserver.slice/[email protected]
             └─44729 /usr/local/bin/rathole -s /opt/rathole/satisfactory.toml

Sep 01 17:56:48 ip-XXX-XX-XX-XXX systemd[1]: Started Rathole Server Service.
Sep 01 17:56:48 ip-XXX-XX-XX-XXX rathole[44729]: 2023-09-01T17:56:48.328045Z  INFO config_watcher{path="/opt/rathole/satisfactory.toml"}: rathole::config_watcher: Start watching the config
Sep 01 17:56:48 ip-XXX-XX-XX-XXX rathole[44729]: 2023-09-01T17:56:48.328569Z  INFO rathole::server: Listening at 0.0.0.0:2333

Before moving on to client setup, we need to make sure the network port for rathole is open on the server for TCP traffic. In AWS that’s done via security group for each instance. I’d recommend to set Source to Custom and restrict it to your own public IP, if you have it static. Otherwise leave it as 0.0.0.0 (publicly open).

Opened network port on AWS

You also need to open game-specific ports we referenced earlier in rathole server config. These need to be open publicly (0.0.0.0) for players to connect. Here’s one example:

Opened network port on AWS

Take note of UDP protocol, which corresponds to game requirements and earlier setup in the config file.

Tip

For some games (this is true for Satisfactory) it may also be necessary to create inbound ICMP rule to allow PING. Otherwise players may not be able to connect, as their game won’t recognize the server as being online. Opened network port on AWS

Client side (game server)

Similarly, we need a client config file. Example from rathole’s repo:

wget https://raw.githubusercontent.com/rapiz1/rathole/main/examples/minimal/client.toml
[client]
remote_addr = "localhost:2333"
default_token = "123"

[client.services.foo1]
local_addr = "127.0.0.1:80"

We need to provide the address of our public server in remote_addr. This can be a domain name if you have a public DNS record pointed to it, or just the IP address. It has to be a public address, accessible over Internet.

We also need to define the same ports used by the game. Since rathole client is running on the game server itself, we expect to connect on localhost (127.0.0.1).

Here’s my setup for Satisfactory, which I’m also going to save as satisfactory.toml in /opt/rathole/, just like with the server config file before:

# client.toml
[client]
remote_addr = "ec2-myOwnInstanceInAWS.eu-west-2.compute.amazonaws.com:2333"
default_token = "SuperSecretUnguessableTokenThatRequiresQuantumComputerToBeBrokenNotReallyButHey"

[client.services.satisfactory-01]
local_addr = "127.0.0.1:15777"
type = "udp"

[client.services.satisfactory-02]
local_addr = "127.0.0.1:15000"
type = "udp"

[client.services.satisfactory-03]
local_addr = "127.0.0.1:7777"
type = "udp"

Change ownership on the config file:

sudo chown rathole:rathole /opt/rathole/satisfactory.toml

Change permissions on the config file, so only rathole user can access it. If you later want to edit it, use sudo:

sudo chmod 600 /opt/rathole/satisfactory.toml

Example service file for the client, save as /etc/systemd/system/[email protected]:

[Unit]
Description=Rathole Client Service
After=network.target

[Service]
User=rathole
Group=rathole
Type=simple
Restart=on-failure
RestartSec=5s
LimitNOFILE=1048576

ExecStart=/usr/local/bin/rathole -c /opt/rathole/%i.toml

[Install]
WantedBy=multi-user.target

Reload daemons:

sudo systemctl daemon-reload

Start the service. Note that after @ symbol I’m again calling the name of the config file I created earlier, satisfactory.toml, this time with rathole-client, as that’s the name of the service file:

sudo systemctl start rathole-client@satisfactory

Confirm status:

sudo systemctl status rathole-client@satisfactory
[email protected] - Rathole Client Service
     Loaded: loaded (/etc/systemd/system/[email protected]; disabled; vendor preset: enabled)
     Active: active (running) since Fri 2023-09-01 17:56:58 UTC; 27s ago
   Main PID: 2309 (rathole)
      Tasks: 14 (limit: 19076)
     Memory: 8.4M
        CPU: 417ms
     CGroup: /system.slice/system-rathole\x2dclient.slice/[email protected]
             └─2309 /usr/local/bin/rathole -c /opt/rathole/satisfactory.toml

Sep 01 17:56:58 sfserver systemd[1]: Started Rathole Client Service.
Sep 01 17:56:58 sfserver rathole[2309]: 2023-09-01T17:56:58.578477Z  INFO handle{service=satisfactory-01}: rathole::client: Starting d7f884da2cd945c4db6ef264c8bab5be4bb44c6f34373c11df8eff37cb7c9664
Sep 01 17:56:58 sfserver rathole[2309]: 2023-09-01T17:56:58.578578Z  INFO handle{service=satisfactory-02}: rathole::client: Starting ed79e28dc79acdafe7fe22076e87108fec54881f686cb8c83b08630e892b5cda
Sep 01 17:56:58 sfserver rathole[2309]: 2023-09-01T17:56:58.578644Z  INFO handle{service=satisfactory-03}: rathole::client: Starting 7ef50bfbb5902cff8cb8f1c259ab2fe3a0e5a67b69fb1e773df0f74529ec78bb
Sep 01 17:56:58 sfserver rathole[2309]: 2023-09-01T17:56:58.578865Z  INFO config_watcher{path="/opt/rathole/satisfactory.toml"}: rathole::config_watcher: Start watching the config
Sep 01 17:56:58 sfserver rathole[2309]: 2023-09-01T17:56:58.651822Z  INFO handle{service=satisfactory-02}:run: rathole::client: Control channel established
Sep 01 17:56:58 sfserver rathole[2309]: 2023-09-01T17:56:58.651927Z  INFO handle{service=satisfactory-03}:run: rathole::client: Control channel established
Sep 01 17:56:58 sfserver rathole[2309]: 2023-09-01T17:56:58.654146Z  INFO handle{service=satisfactory-01}:run: rathole::client: Control channel established

That’s it! Assuming everything went OK, you can point players to your public server IP address or DNS record, or try to connect yourself. It should work seamlessly, as if you’re connecting to the game server itself.