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:
┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│00000000│ ff ff ff ff 54 53 6f 75 ┊ 72 63 65 20 45 6e 67 69 │××××TSou┊rce Engi│
│00000010│ 6e 65 20 51 75 65 72 79 ┊ 00 │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.
┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│00000000│ ff ff ff ff 41 6a 81 08 ┊ 6c │××××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.
┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│00000000│ ff ff ff ff 49 11 44 61 ┊ 79 5a 20 55 53 20 2d 20 │××××I•Da┊yZ US - │
│00000010│ 4e 59 20 36 30 35 33 20 ┊ 28 31 73 74 20 50 65 72 │NY 6053 ┊(1st Per│
│00000020│ 73 6f 6e 20 4f 6e 6c 79 ┊ 29 00 63 68 65 72 6e 61 │son Only┊)⋄cherna│
│00000030│ 72 75 73 70 6c 75 73 00 ┊ 64 61 79 7a 00 44 61 79 │rusplus⋄┊dayz⋄Day│
│00000040│ 5a 00 00 00 23 3c 00 64 ┊ 77 00 01 31 2e 32 33 2e │Z⋄⋄⋄#<⋄d┊w⋄•1.23.│
│00000050│ 31 35 37 30 34 35 00 b1 ┊ 74 27 03 3c ad 93 b4 62 │157045⋄×┊t'•<×××b│
│00000060│ 40 01 62 61 74 74 6c 65 ┊ 79 65 2c 6e 6f 33 72 64 │@•battle┊ye,no3rd│
│00000070│ 2c 73 68 61 72 64 30 30 ┊ 31 2c 6c 71 73 30 2c 65 │,shard00┊1,lqs0,e│
│00000080│ 74 6d 34 2e 32 30 30 30 ┊ 30 30 2c 65 6e 74 6d 34 │tm4.2000┊00,entm4│
│00000090│ 2e 30 30 30 30 30 30 2c ┊ 31 34 3a 30 39 00 ac 5f │.000000,┊14:09⋄×_│
│000000a0│ 03 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.
┏━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━┯━━━━━━━━┯━━━━━━━━━━━━━━━┓
┃00000000│ ff ff ff ff ┊ │xxxx ┊ │ Response Type ┃
┠┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┨
┃00000004│ 49 ┊ │I ┊ │ Packet Type ┃
┠┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┨
┃00000005│ 11 ┊ │x ┊ │ Protocol ┃
┠┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┨
┃00000006│ 44 61 79 5a 20 55 53 20 ┊ 2d 20 4e 59 20 36 30 35 │DayZ US ┊- NY 605│ Name ┃
┃00000022│ 33 20 28 31 73 74 20 50 ┊ 65 72 73 6f 6e 20 4f 6e │3 (1st P┊erson On│ ┃
┃00000038│ 6c 79 29 00 ┊ │ly)⋄ ┊ │ ┃
┠┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┨
┃00000042│ 63 68 65 72 6e 61 72 75 ┊ 73 70 6c 75 73 00 │chernaru┊splus⋄ │ Map ┃
┠┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┨
┃00000056│ 64 61 79 7a 00 ┊ │dayz⋄ ┊ │ Folder ┃
┠┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┨
┃00000061│ 44 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.
┏━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━┯━━━━━━━━┯━━━━━━━━━━━━━━━┓
┃00000066│ 00 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.
┏━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━┯━━━━━━━━┯━━━━━━━━━━━━━┓
┃00000068│ 23 ┊ │# ┊ │ Players ┃
┠┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┨
┃00000069│ 3c ┊ │< ┊ │ Max Players ┃
┠┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┨
┃00000070│ 00 ┊ │⋄ ┊ │ Bots ┃
┠┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┨
┃00000071│ 64 ┊ │d ┊ │ Server Type ┃
┠┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┨
┃00000072│ 77 ┊ │w ┊ │ Environment ┃
┠┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┨
┃00000073│ 00 ┊ │⋄ ┊ │ Visibility ┃
┠┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┨
┃00000074│ 01 ┊ │x ┊ │ VAC ┃
┠┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┼┄┄┄┄┄┄┄┄┄┄┄┄┄┨
┃00000075│ 31 2e 32 33 2e 31 35 37 ┊ 30 34 35 00 │1.23.157┊045⋄ │ 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.
┏━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━┯━━━━━━━━┯━━━━━┓
┃00000087│ b1 ┊ │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
.
┏━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━┯━━━━━━━━┯━━━━━━━━━━━┓
┃00000088│ 74 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.
┏━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━┯━━━━━━━━┯━━━━━━━━━━┓
┃00000090│ 03 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.
┏━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━┯━━━━━━━━┯━━━━━━━━━━┓
┃00000094│ b4 62 40 01 62 61 74 74 ┊ 6c 65 79 65 2c 6e 6f 33 │xb@xbatt┊leye,no3│ Keywords ┃
┃00000110│ 72 64 2c 73 68 61 72 64 ┊ 30 30 31 2c 6c 71 73 30 │rd,shard┊001,lqs0│ ┃
┃00000126│ 2c 65 74 6d 34 2e 32 30 ┊ 30 30 30 30 2c 65 6e 74 │,etm4.20┊0000,ent│ ┃
┃00000142│ 6d 34 2e 30 30 30 30 30 ┊ 30 2c 31 34 3a 30 39 00 │m4.00000┊0,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.
┏━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━┯━━━━━━━━┯━━━━━━━━━┓
┃00000158│ ac 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.