jmhobbs

DayZ Server Browsers

Recently I've been playing a lot of DayZ, often through the DayZSA launcher. But I was curious, where do these server listings live, and how does a thrid-party launcher get them? Further, how does a website like BattleMetrics get them and list it online?

This question led me to some very, very weird data structures, jammed into weird protocols, that feel utterly undesigned. And yet, they work!

A2S, or Steam Server Queries

Many multiplayer games use a protocol provided for in the Steam SDK commonly known as A2S. Some light searching revealed that DayZ uses this protcol. It is UDP based, and loosely documented, the best I could find was this page on the Valve wiki

If you skim that, you'll see it feels very ad-hoc, like it started small and simple and things just got glommed onto it over time. So let's try to talk to a DayZ server!

First, we need a server. To keep things simple, we will use an official server, NY 6053. At time of writing it's query port was at 31.214.136.147:10101.

Now, we need to know how what it expects. We will start with A2S_INFO, to get the map, players etc.

The query starts with a four byte packet type header, 0xFFFFFFFF. This signifies it is a single packet request or response, not split across multiple packets.

Next, there is a single byte header indicating the request type, in this case 0x54 for A2S_INFO. Finally, we have the query string, which is Source Engine Query\0. Strings in this protocol are null terminated, like in C.

So the expected query packet is:

┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│00000000ff ff ff ff 54 53 6f 7572 63 65 20 45 6e 67 69××××TSource Engi│
│000000106e 65 20 51 75 65 72 7900                     ne Query│
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘

Let's send that with netcat,

$ printf '\xFF\xFF\xFF\xFF\x54Source Engine Query\0' | nc --udp -x 31.214.136.147 10101
Sent 25 bytes to the socket
00000000  FF FF FF FF  54 53 6F 75  72 63 65 20  45 6E 67 69  ....TSource Engi
00000010  6E 65 20 51  75 65 72 79  00                        ne Query.
AlReceived 9 bytes from the socket
00000000  FF FF FF FF  41 6A 81 08  6C                        ....Aj..l

That...is not a long response. It turns out, some A2S servers use a challenge on the query, something that was tacked onto the protocol later on. This is a DDoS prevention mechanism for a reflection attack.

Before we can handle this challenge, let's take a look at the response packet. We have the 4 byte header indicating a single packet response. Then 0x41, which is our response type, S2C_CHALLENGE. Finally, there is the four byte challenge. This is the value we need to append to our original packet to authenticate with the server.

┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│00000000ff ff ff ff 41 6a 81 086c                     ××××Aj×l       │
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘

At this point, we've reached the end of what is really viable to do with netcat, since we need to send raw bytes in response, and our little printf shenanigans won't get us there anymore. Let's write a little client in Go instead!

package main

import (
"bytes"
"fmt"
"net"
"os"
)

func main() {
var queryPacket bytes.Buffer
// packet type
queryPacket.Write([]byte{0xFF, 0xFF, 0xFF, 0xFF})
// request type
queryPacket.WriteByte(0x54)
// payload
queryPacket.WriteString("Source Engine Query\000")

conn, err := net.Dial("udp", "31.214.136.147:10101")
if err != nil {
panic(err)
}
defer conn.Close()

response := sendPacket(conn, queryPacket.Bytes())

if response[4] == 0x41 {
fmt.Println("Challenge packet received")
challenge := response[5:]
fmt.Printf("Challenge: %v\n", challenge)

// add the challenge bytes
queryPacket.Write(challenge)

response = sendPacket(conn, queryPacket.Bytes())
} else {
fmt.Println("Challenge packet not received")
}

fmt.Printf("Response: %v\n", response)
}

func sendPacket(conn net.Conn, pkt []byte) []byte {
_, err := conn.Write(pkt)
if err != nil {
panic(err)
}

response := make([]byte, 1024)

n, err := conn.Read(response)
if err != nil {
panic(err)
}

return response[:n]
}

There's a lot of fluff in there, and a lot of assumptions, but look, it works!

$ go run main.go
Challenge packet received
Challenge: [53 154 72 189]
Response: [255 255 255 255 73 17 68 97.....]

A2S_INFO - Basic server information

Now that we have some bytes, let's decode it. Here it is again in as a hex dump.

┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│00000000ff ff ff ff 49 11 44 6179 5a 20 55 53 20 2d 20××××IDayZ US - │
│000000104e 59 20 36 30 35 33 2028 31 73 74 20 50 65 72NY 6053 (1st Per│
│0000002073 6f 6e 20 4f 6e 6c 7929 00 63 68 65 72 6e 61son Only)cherna│
│0000003072 75 73 70 6c 75 73 0064 61 79 7a 00 44 61 79rusplusdayzDay│
│000000405a 00 00 00 23 3c 00 6477 00 01 31 2e 32 33 2eZ⋄⋄⋄#<dw1.23.│
│0000005031 35 37 30 34 35 00 b174 27 03 3c ad 93 b4 62157045×t'<×××b│
│0000006040 01 62 61 74 74 6c 6579 65 2c 6e 6f 33 72 64@battleye,no3rd│
│000000702c 73 68 61 72 64 30 3031 2c 6c 71 73 30 2c 65,shard001,lqs0,e│
│0000008074 6d 34 2e 32 30 30 3030 30 2c 65 6e 74 6d 34tm4.200000,entm4│
│000000902e 30 30 30 30 30 30 2c31 34 3a 30 39 00 ac 5f.000000,14:09×_│
│000000a003 00 00 00 00 00      ⋄⋄⋄⋄⋄  │
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘

It starts with the "single packet" preamble which we've seen on every message so far. The response type on this one is 0x49, which is the A2S_INFO response value. Next we have a single byte for "protocol". I am unsure what this is honestly, there is nothing in the docs. After that is the name, a null terminated string, DayZ US - NY 6053 (1st Person Only). Then map, folder, and game name, all strings.

┏━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━┯━━━━━━━━┯━━━━━━━━━━━━━━━┓
┃00000000ff ff ff ff             ┊                         │xxxx    ┊        │ Response Type ┃
┠┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┨
┃0000000449                      ┊                         │I       ┊        │ Packet Type   ┃
┠┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┨
┃0000000511                      ┊                         │x       ┊        │ Protocol      ┃
┠┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┨
┃0000000644 61 79 5a 20 55 53 202d 20 4e 59 20 36 30 35DayZ US - NY 605│ Name          ┃
┃0000002233 20 28 31 73 74 20 5065 72 73 6f 6e 20 4f 6e3 (1st Person On│               ┃
┃000000386c 79 29 00             ┊                         │ly)    ┊        │               ┃
┠┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┨
┃0000004263 68 65 72 6e 61 72 7573 70 6c 75 73 00chernarusplus  │ Map           ┃
┠┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┨
┃0000005664 61 79 7a 00          ┊                         │dayz   ┊        │ Folder        ┃
┠┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┨
┃0000006144 61 79 5a 00          ┊                         │DayZ   ┊        │ Game          ┃
┗━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━┷━━━━━━━━┷━━━━━━━━━━━━━━━┛

After that is a short, a two byte integer, for the Steam App ID. In this response that is 00 00, which doesn't match up with DayZ's actual app id, 221100. This exposes a flaw in the protocol. The maximum value you can store in an unsigned short is 65535, which is way smaller than our app id. So since it doesn't fit, the server returns a 0. We will come back to this later on.

┏━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━┯━━━━━━━━┯━━━━━━━━━━━━━━━┓
┃0000006600 00                   ┊                         │      ┊        │ Steam App ID  ┃
┗━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━┷━━━━━━━━┷━━━━━━━━━━━━━━━┛

Following this are byte fields for number of players, maximum player count, and bots on the server. At the time there were 35/60 players and no bots. We also get bytes indicating if the server is dedicated, what OS it's hosted on, if it is password protected, and if it is VAC enabled. There's a weird mix here between using ASCII characters (w for Windows servers!) and numeric values (1 means VAC is on!). I understand the reasoning, but this is not a human readable format, so it's a bit odd to mix it up like that. Lastly we get a version number, as a string.

┏━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━┯━━━━━━━━┯━━━━━━━━━━━━━┓
┃0000006823                      ┊                         │#       ┊        │ Players     ┃
┠┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┨
┃000000693c                      ┊                         │<       ┊        │ Max Players ┃
┠┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┨
┃0000007000                      ┊                         │       ┊        │ Bots        ┃
┠┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┨
┃0000007164                      ┊                         │d       ┊        │ Server Type ┃
┠┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┨
┃0000007277                      ┊                         │w       ┊        │ Environment ┃
┠┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┨
┃0000007300                      ┊                         │       ┊        │ Visibility  ┃
┠┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┨
┃0000007401                      ┊                         │x       ┊        │ VAC         ┃
┠┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┨
┃0000007531 2e 32 33 2e 31 35 3730 34 35 001.23.157045    │ Version     ┃
┗━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━┷━━━━━━━━┷━━━━━━━━━━━━━┛

Now we get to an interesting byte, the EDF, or Extra Data Flag. This is a bitfield which indicates more (and which) fields follow.

┏━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━┯━━━━━━━━┯━━━━━┓
┃00000087b1                      ┊                         │x       ┊        │ EDF ┃
┗━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━┷━━━━━━━━┷━━━━━┛

Our EDF byte is 0xb1, which is 10110001. We take this value and apply a bitwise and operation, &, with other values to check which flags are set. First up is 0x80, or 10000000.

  10110001
& 10000000
----------
  10000000

This flag is on! This means there will be a short containing the servers game port. We do this with 0x10, 0x40, 0x20, 0x01 and can parse the remaining bytes. We have game port, Steam ID, keywords and game id.

Game port gives us our first meaningful use of a multi-byte integer value (since Steam App ID was just 00 00). We have two ways to interpret this value: little-endian or big-endian. Essentially, which "end" of a number comes first? The docs tell us what the byte order will be little-endian, which is unusual for a network protocol, they tend towards big-endian. This means 74 27 is the integer value 10100 (don't be fooled, it's not binary!). If it were big-endian the value would be 29735.

┏━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━┯━━━━━━━━┯━━━━━━━━━━━┓
┃0000008874 27                   ┊                         │t'      ┊        │ Game Port ┃
┗━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━┷━━━━━━━━┷━━━━━━━━━━━┛

Next we have a long, which is a four byte, or 32 bit integer. It is 03 3c ad 93, which comes out to 2477603843. This is the SteamID of the server, which is a unique identifier for a Steam account, not for an app.

┏━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━┯━━━━━━━━┯━━━━━━━━━━┓
┃0000009003 3c ad 93             ┊                         │x<xx    ┊        │ Steam ID ┃
┗━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━┷━━━━━━━━┷━━━━━━━━━━┛

Following that we have a string of "keywords". These are tags for describing the server. The string we get is comma separated, and rather opaque. Some things that stand out are shard001 and what looks like a time, 14:09. A hive in DayZ is a group of servers that a character can migrate in between, and the player state is stored on a shard. Beyond that I'm not sure what any of the rest of it is.

┏━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━┯━━━━━━━━┯━━━━━━━━━━┓
┃00000094b4 62 40 01 62 61 74 746c 65 79 65 2c 6e 6f 33xb@xbattleye,no3│ Keywords ┃
┃0000011072 64 2c 73 68 61 72 6430 30 31 2c 6c 71 73 30rd,shard001,lqs0│          ┃
┃000001262c 65 74 6d 34 2e 32 3030 30 30 30 2c 65 6e 74,etm4.200000,ent│          ┃
┃000001426d 34 2e 30 30 30 30 3030 2c 31 34 3a 30 39 00m4.000000,14:09│          ┃
┗━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━┷━━━━━━━━┷━━━━━━━━━━┛

Lastly, we have Game ID, which is a 32-bit integer, representing the Appd ID. App ID's apparently were originally 16 bits, as mentioned above. Since the number of apps grew too large, they moved to 32 bits, but couldn't alter the protocol. So when Game ID is present in the EDF, it supercedes the value in the App ID field. According to the docs, the App ID is in the lower 24 bits of this field, so just 03 5F AC.

I'm not sure why they chose to just use three bytes for App ID, since we're sending all four bytes anyway. It is fairly safe to assume that this field will have 0's in the first byte, so you can cast it as a long int, and the whole thing will be valid. The value we get is 00000158ac5f03, which is 221100 in decimal. This is the App ID for DayZ.

┏━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━┯━━━━━━━━┯━━━━━━━━━┓
┃00000158ac 5f 03 00             ┊                         │x_x    ┊        │ Game ID ┃
┗━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━┷━━━━━━━━┷━━━━━━━━━┛

Next Steps

That is it for A2S_INFO. There's a lot in there, but not everything we see in tools like DayZSA. This post has gotten long, so I'm going to stop here, but in the next post I'll cover A2S_RULES which has a lot more data crammed into it. Eventually I may get into server discovery and CM Client, but we will see.