Binary Data in Gleam: Implementing The RCON Protocol
Tags: gleam, erlang, elixir, gamedev
Published: 2024-03-16 23:08:49

**Update**: This is now a library! Check it out [here](https://github.com/bitcrshr/glame)

With Gleam's 1.0 release [hot off the presses](https://gleam.run/news/gleam-version-1/) and coverage by Twitch and Youtube giants [ThePrimeagen](https://youtu.be/9mfO821E7sE?si=_MXCmnRGpT34xNRA) and [Theo](https://youtu.be/_I-CSgoCgsk?si=nSS5a-fgqLryXjjw), many folks are being drawn to the language as a gentle but *powerful* introduction to functional programming, and I'm no exception.

One of my go-to projects for learning a new language is implementing [Valve's Source RCON Protocol](https://developer.valvesoftware.com/wiki/Source_RCON_Protocol) and in this article we're gonna learn how to do it in Gleam!

We're going to base our implementation off of the excellent RCON package for Go known as [gorcon](https://github.com/gorcon/rcon).

Let's get started!

## What the hell is Gleam?

Gleam is an *impure* functional programming language built off of Erlang's VM, [BEAM](https://www.erlang.org/blog/a-brief-beam-primer/) with a focus on simplicity and developer experience. It has interoperability with other BEAM languages (e.g., Erlang and Elixir) and can even compile to Javascript!

For more information, I highly encourage you to check out the [language tour](https://tour.gleam.run). 

## What the hell is RCON?

RCON stands for Remote Connection, and is a binary protocol developed by Steam as part of Source. It is used by many popular game servers such as Rust, Minecraft, and PalWorld, and allows you to execute commands like `ShowPlayers` (PalWorld) from a remote system (such as the game client).

It has a simple wire format. In order, the fields are:
* `size` - A 32-bit little-endian signed integer representing the total size of the packet (minus itself)
* `id` - Another i32-LE that can be set to any positive value. This allows a response to be matched to a request. 
* `type` - Another i32-LE representing what this packet is meant to do. There are four types that are widely supported:
	* `SERVERDATA_AUTH (3)` - Sent by the client with the password as the body to authenticate the connection to the RCON server.
	* `SERVERDATA_AUTH_RESPONSE (2)` - Sent by the server to indicate the auth status following a `SERVERDATA_AUTH` request.
	* `SERVERDATA_EXECCOMMAND (2)` - Sent by the client to execute a command (such as `ShowPlayers`) on the server.
		* That's not a typo--both `SERVERDATA_AUTH_RESPONSE` and `SERVERDATA_EXECCOMMAND` are represented by a `2`!
	* `SERVERDATA_RESPONSE_VALUE (0)` - Sent by the server to indicate the result of a `SERVERDATA_EXECCOMMAND` request.
* `body` - An ASCII string that is terminated by two null bytes[^1]

All of these fields together make a packet!

## Implementing Packets

To begin, let's define some constants and helper functions that will help us later:

```rust
//// src/packet.gleam

/// How many bytes the padding (i.e., <<0x00, 0x00>>) takes up
const packet_padding_size_bytes: Int = 2

/// How many bytes the header (i.e., the id and type) takes up
const packet_header_size_bytes: Int = 8

/// Returns the byte size of the smallest possible packet, which is a packet
/// with an empty body.
fn min_packet_size_bytes() -> Int {
	packet_padding_size_bytes + packet_header_size_bytes
}

/// Returns the byte size of the largest possible packet, which is a packet
/// with a 4KB body. This is a limitation set by the protocol.
fn max_packet_size_bytes() -> Int {
	4096 + min_packet_size_bytes
}
```

Next, we'll define some types and helper functions that will make the public interface a bit nicer:

```rust
//// src/packet.gleam

// ...

/// Represents valid RCON packet types.
pub type PacketType {
	ServerDataAuth
	ServerDataAuthResponse
	ServerDataExecCommand
	ServerDataResponseValue
}

pub fn packet_type_to_int(pt: PacketType) -> Int {
  case pt {
    ServerDataAuth -> 3
    ServerDataAuthResponse | ServerDataExecCommand -> 2
    ServerDataResponseValue -> 0
  }
}

/// Represents an RCON packet.
pub type Packet {
	Packet(size: Int, id: Int, typ: Int, body: BitArray)
}

/// Constructs a new Packet.
pub fn new(
  packet_type: PacketType,
  packet_id: Int,
  body: String,
) -> Result(Packet, String) {
  let size =
    string.byte_size(body)
    + packet_header_size_bytes
    + packet_padding_size_bytes
  let max = max_packet_size_bytes()
  
  // note: it would be good to check if the body is ASCII here too!

  case size {
    _ if size > max -> Error("body is larger than 4096 bytes")
    _ -> {
      let bytes =
        body
        |> bytes_builder.from_string
        |> bytes_builder.to_bit_array

      Ok(Packet(size, packet_id, packet_type_to_int(packet_type), bytes))
    }
  }
}
```

Great! Now we have a solid representation of a Packet in Gleam. As a bonus, as long as we use `new(...)`, any Packet we construct is guaranteed to work with the protocol. 

One cool bit of Gleam syntax we used is the pipe operator `|>`, and it would be an injustice to not go over it quickly:

```rust
let bytes = 
	body
	|> bytes_builder.from_string // parens optional when fn takes 1 arg
	|> bytes_builder.to_bit_array

// is equivalent to:
let bytes = bytes_builder.to_bit_array(
	bytes_builder.from_string(
		body	
	)
)
```

This allows us to "chain" function calls in a way where that *feels* like a builder pattern but is really just syntactic sugar for nested function calls. Neat, right?

Now, let's get to the meat of it and implement the binary bits[^2]! First, let's take advantage of the pipe operator again to convert a Packet into its binary format:

```rust
//// src/packet.gleam

// ...

pub fn to_bytes(packet: Packet) -> BitArray {
  bytes_builder.new()
  |> bytes_builder.append(<<packet.size:int-size(32)-little>>)
  |> bytes_builder.append(<<packet.id:int-size(32)-little>>)
  |> bytes_builder.append(<<packet.typ:int-size(32)-little>>)
  |> bytes_builder.append(packet.body)
  |> bytes_builder.append(<<0x00, 0x00>>)
  |> bytes_builder.to_bit_array()
}
```

To again avoid grave injustices, let's examine the syntax for `BitArray` literals:

```rust
let size_bytes = <<packet.size:int-size(32)-little>>
```

What we're saying here is:
* `packet.size` - Write packet.size as binary to the `BitArray`
* `int` - Represent it as an int
* `size(32)` - With a size of 32 bits
* `little` - As little-endian

Finally, let's use Gleam's *excellent* pattern matching to read a `Packet` from a `BitArray`. If you're familiar with Rust, you'll find this to be very similar to `match` :

```rust
//// src/packet.gleam

// ...

pub fn from_bytes(bytes: BitArray) -> Result(Packet, String) {
  let min_ps = min_packet_size_bytes()
  let max_ps = max_packet_size_bytes()

  // notice how pattern matching allows us to do this declaritively!
  // we're basically saying "if the data is this shape, do this"
  // instead of worrying about the details
  case bytes {
	// pull off an i32-LE called `size` and leave the `rest` as a BitArray.
	// we need to read the size first so we can know how many bytes the body 
	// should be!
    <<size:size(32)-little-int, rest:bits>> -> {
      case size {
        _ if size < min_ps -> {
          Error("size cannot be less than min_packet_size")
        }
        _ if size > max_ps ->
          Error("size cannot be greater than max_packet_size")
        _ -> {
          let body_size_bits =
            { size - packet_header_size_bytes - packet_padding_size_bytes } * 8

          case rest {
		    // pull off the rest of the fields!
		    // note that this is also ensuring there isn't any "extra"
		    // data left over in the BitArray.
            <<
              id:int-size(32)-little,
              typ:int-size(32)-little,
              body:size(body_size_bits)-bits,
              padding:size(16)-bits,
            >> -> {
              case typ {
                3 | 2 | 0 -> {
                  case padding {
                    <<0x00, 0x00>> -> {
                      Ok(Packet(size, id, typ, body))
                    }

                    _ -> Error("padding must be <<0x00, 0x00>>")
                  }
                }

                _ -> Error("type must be 3, 2, or 0")
              }
            }

            _ -> {
              Error("invalid packet format")
            }
          }
        }
      }
    }

    _ -> Error("invalid packet format")
  }
}

```

And that's all there is to it! Let me know in the comments if you'd like to see a part 2 where we can write the TCP logic to make a full-fledged RCON client in Gleam. 

Better at Gleam than I am? Leave me a comment about how I could improve!

## Who the hell are you anyway?

I'm Chandler, and my background is in Go, Rust, Zig, and begrudgingly, Typescript. I started out as a frontend engineer, but was quickly captivated by the realm of backend and systems programming. Professionally, I work as a core engineer at a healthcare startup. In my free time, I enjoy playing video games, learning new programming languages, and playing guitar.

You can catch me on [**𝕏** Formerly Known As Twitter](https://x.com/bitcrshr) and check out my project graveyard on [Github](https://github.com/bitcrshr).


[^1]: Technically, it's a null-terminated string (ASCIIZ) followed by another null byte to terminate the packet
[^2]: Pun was *absolutely* intended. You're welcome.