RE:NODE

Networking13 min read

Query ports and A2S: server status queries

What the query port answers, how Valve's A2S protocol and its challenge work, how to query any Steam server from a script, and why a query fails while the game works.

0 readers

A query port is the port a game server uses to answer the question "who are you and how full are you?" - for server browsers, status sites, Discord bots and monitoring. On Steam games the answer comes over A2S, a small UDP protocol from Valve: the client sends a fixed request, the server replies with its name, map, player count, maximum players and a handful of flags. It is separate from gameplay, often on a separate port, and unauthenticated by design. Knowing how it works lets you test a server's visibility with one command, tell a port problem from a listing problem, and understand why the same port is both how players find you and how attackers use you.

What the query port is for#

Gameplay traffic only flows once a player has decided to join. Everything before that - the browser list, the player count on a website, the "server is online" message in Discord - comes from the query protocol. It has three properties that matter:

  • It answers strangers. Anyone who sends a correctly formed packet gets a reply. It has to, because the server browser is a stranger.
  • It is UDP. One request, one reply (or a few for large answers), no connection.
  • It is often on its own port. Source-engine games answer on the game port itself. Many other Steam games answer on a separate query port, usually a fixed offset from the game port or a fixed number such as 27015.

The third point is behind a large share of "the server works but nobody can see it" reports. The player types the game port, which is open; the browser asks the query port, which is not. The list side of that story is in why a server does not show in the server list. This post is the protocol side.

A2S in one table#

Every A2S packet starts with four bytes of FF FF FF FF, which marks a single, unsplit packet. Then comes a one-byte type. These are the three requests that matter today:

RequestType byteReply byteWhat you get
A2S_INFO0x54 (T)0x49 (I)Name, map, game, players, max players, bots, flags, version
A2S_PLAYER0x55 (U)0x44 (D)Each player's name, score and time connected
A2S_RULES0x56 (V)0x45 (E)Server rules: public cvars or key-value pairs

A2S_INFO carries the string Source Engine Query followed by a zero byte. The reply is a run of fields in a fixed order:

FieldTypeNotes
ProtocolbyteProtocol version
Name, Map, Folder, GamestringsZero-terminated; folder is the game directory, such as cstrike
IDshortApp id, truncated to 16 bits
Players, Max players, BotsbytesBots are included in Players
Server typebyted dedicated, l listen (non-dedicated), p SourceTV relay
Environmentbytel Linux, w Windows, m or o Mac
Visibilitybyte0 public, 1 password protected
VACbyte0 unsecured, 1 secured
VersionstringThe game version string
Extra data flagbyteSays which optional fields follow

The optional fields after the flag include the game port (flag 0x80), the server's Steam ID (0x10), the SourceTV port and name (0x40), keywords or tags (0x20) and the full 64-bit game id (0x01). The game port field is how the Steam browser can be pointed at a query port and still connect players to the right game port.

Large replies - long player lists, long rules lists - are split across several packets. Split packets start with FE FF FF FF instead, followed by an id, a total count and a sequence number, and the client reassembles them. Rules on a heavily modded Source server are the usual case.

Two older requests, A2S_PING and the standalone challenge request, are deprecated and many servers no longer answer them. Do not build tools on them.

The challenge, and why it was added#

A2S was designed in an era that did not worry about reflection. A request of about 25 bytes from a forged source address produced a reply of a few hundred bytes sent to the forged address - an amplifier anybody could aim. Game servers became a common ingredient in reflection attacks, which DDoS attacks on game servers explained covers in detail.

The fix is a challenge. A2S_PLAYER and A2S_RULES have required one for a long time, and since late 2020 Valve's implementation also challenges A2S_INFO. The exchange looks like this:

  1. The client sends the request.
  2. The server replies with a short packet of type 0x41 (A) carrying a 4-byte challenge number.
  3. The client sends the same request again with those 4 bytes appended.
  4. The server sends the real reply.

A forged source address never sees step 2, so it never gets step 4, and the amplification disappears. The practical consequence for you: an old status script that sends one A2S_INFO and expects an I reply now receives an A reply, decides the server is broken, and reports it offline. If a monitoring tool went quiet in the last few years and nothing else changed, this is likely why. Servers on older engines or custom implementations may still answer without a challenge, so tools must handle both.

For A2S_PLAYER and A2S_RULES, the first request carries FF FF FF FF in the challenge position, which asks for a challenge.

Querying a server yourself#

Testing from the server proves nothing, because loopback never blocks anything. Run these from another machine on another network.

With python-a2s

The quickest reliable tool is the python-a2s package, which handles challenges and split packets:

bash
$ pip install python-a2s$ python3
python
import a2saddress = ("203.0.113.10", 27015)  # the QUERY port, not always the game portinfo = a2s.info(address, timeout=3.0)print(info.server_name, info.map_name, info.player_count, info.max_players)for player in a2s.players(address):    print(player.name, player.score, round(player.duration))print(a2s.rules(address))

A socket.timeout means no reply at all: the port is closed, filtered, wrong, or the server is not answering queries. A reply with the wrong server name means you are querying something else on that address - two servers on adjacent ports is a common trap.

Without any library

If you cannot install anything, the protocol is small enough to speak by hand. This does A2S_INFO with the challenge and reads the first few fields; it ignores split packets, which an info reply almost never needs.

a2s_info.py
import socket, struct, sysdef read_str(buf, i):    end = buf.index(b"\x00", i)    return buf[i:end].decode("utf-8", "replace"), end + 1host, port = sys.argv[1], int(sys.argv[2])req = b"\xFF\xFF\xFF\xFFTSource Engine Query\x00"s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)s.settimeout(3.0)s.sendto(req, (host, port))data, _ = s.recvfrom(4096)if data[4] == 0x41:  # challenge: resend with it appended    s.sendto(req + data[5:9], (host, port))    data, _ = s.recvfrom(4096)i = 6  # skip the 4-byte header, the 0x49 type and the protocol bytename, i = read_str(data, i)map_name, i = read_str(data, i)folder, i = read_str(data, i)game, i = read_str(data, i)app_id, players, max_players, bots = struct.unpack_from("<HBBB", data, i)print(f"{name} | {map_name} | {players}/{max_players} ({bots} bots)")

Run it as python3 a2s_info.py 203.0.113.10 27015.

With GameDig

GameDig is a Node.js library and command-line tool that knows hundreds of games, including ones that do not speak A2S. It is what many status bots and websites use underneath.

bash
$ npm install -g gamedig$ gamedig --type valheim 203.0.113.10:2456

Game ids and port handling have changed between major versions - some ids were renamed, and for many games GameDig applies the known offset from the game port to the query port itself. Check the games list for the version you installed before assuming a timeout means a dead server.

Query ports by game#

The offset between game port and query port is a per-game convention. These are defaults; most are configurable, and the server's startup log is the final authority.

GameGame portQuery portProtocol
CS2, TF2, Garry's Mod, Left 4 Dead 227015same portA2S
Counter-Strike 1.627015same portA2S (GoldSrc)
Valheim24562457 (game + 1)A2S
Arma 323022303 (game + 1)A2S
Unturned2701627015 (the configured Port)A2S
Palworld821127015A2S
Project Zomboid16261same portA2S
Rust28015same portA2S
Minecraft Java25565same port (TCP status)Server List Ping

The shape to remember: Source games answer on the game port, and games that bolted Steam's server API onto another engine usually answer on a second port. When you move a game port, check whether the query port moves with it (an offset) or stays put (a fixed setting). Changing one and forgetting the other produces a server that is joinable and invisible. On a panel, both numbers need to be allocated - on RE:NODE they are part of the plan's port count and extra ones are added on the Network tab - and the game's own setting must match what was allocated. Game server ports explained has the longer table, including RCON and auxiliary ports.

Minecraft does it differently#

Minecraft Java does not use A2S. It has two mechanisms, and they are easy to confuse.

Server List Ping is what the multiplayer screen uses. It runs over TCP on the game port: the client sends a handshake asking for status, then a status request, and the server answers with JSON - version, online and maximum players, a sample of player names, the MOTD and the server icon. It is controlled by enable-status in server.properties, which is on by default. Turning it off makes the server look offline in the list while still accepting players who know the address.

Query is an older UDP protocol, often called GameSpy4 query after its origin. It is off by default and controlled by enable-query and query.port. When on, it returns more detail than the status ping, including the plugin list on some server software. Third-party status sites sometimes ask for it; almost nothing else needs it.

python
from mcstatus import JavaServer  # pip install mcstatusserver = JavaServer.lookup("play.example.com:25565")status = server.status()print(status.players.online, status.players.max, round(status.latency))

lookup also follows an SRV record, so it tests the address the way players type it. SRV records for Minecraft explains that part.

What status sites, bots and lists do with it#

Every server list website, every "players online" widget and almost every Discord status bot is a query client on a timer. They send A2S (or the game's equivalent) every minute or so and store the answer. That has three consequences.

First, they see what the query port says, nothing more. If the query port is closed, they report you offline even while people are playing. Second, the player names you see on a tracking site come straight from A2S_PLAYER, so they are public to anyone - which is worth knowing if you run a server for people who would rather not be listed. Third, a status bot querying every thirty seconds is harmless; fifty of them, plus every list site, plus scanners, add up to a steady background of query traffic that you will see in packet counters even on an empty server.

If you want a status message in Discord without a third-party bot, Discord webhooks for server status shows how to post one from a script like the one above.

Rate limits and what the query gives away#

Because the query port answers anyone, it is the cheapest part of a server to flood. Source-engine servers have built-in limits, with defaults that suit normal use:

server.cfg
sv_max_queries_sec 3          // queries per second from one addresssv_max_queries_window 30      // window over which that rate is averagedsv_max_queries_sec_global 60  // total queries per second answered

Lowering the global limit protects the server during a flood at the cost of looking unresponsive to some legitimate browsers. Raise it back afterwards, or you will spend a week wondering why the server drops out of the list.

The query also leaks more than people expect. A2S_RULES on a Source server returns every cvar flagged as public, which on a modded server often includes plugin names and versions - a free inventory for anyone looking for a known vulnerable plugin. Some games and admin frameworks let you hide rules or the player list; where they do, consider it for a private server. What we do about attacks covers the query port from the defensive side.

Turning a query into a health check#

A query is the best cheap health check a game server has, better than an ICMP ping or a bare TCP connect, because only the running game process can answer it. A machine that is up with a crashed or frozen server still answers pings; it does not answer A2S. Used properly it catches the failures that matter to players.

A few rules make it trustworthy:

  • Check from outside. A check running on the same machine tells you about the process, not about the path players use. Run it from somewhere else, ideally a different network.
  • Allow for one lost packet. UDP replies get lost. Retry once with a longer timeout before counting a failure, and alert after three consecutive failures, not one.
  • Check the content, not just the reply. Compare the server name and the maximum players with what you expect. A reply from the wrong server - or from a server that restarted with a default config after an update wiped yours - is a failure that a simple "did it answer" check misses.
  • Watch the trend, not the instant. A player count that drops to zero at the same time every night is a scheduled restart. One that drops to zero at random and comes back two minutes later is a crash loop, and why your game server keeps restarting is the next read.
  • Keep the interval sensible. Once a minute is plenty. Your own monitor querying every second is indistinguishable from a small flood and may trip the server's rate limit.

A query proves the process answers. It does not prove that players can complete a join, which also depends on the game port, the version and any mods. Treat it as the first line of monitoring, not the only one; monitoring that tells you something covers the rest.

When the query fails but the game works#

SymptomLikely cause
Timeout, but players can joinQuery port not allocated, not forwarded, or UDP blocked
Reply from the wrong serverQuerying the wrong port on a shared address
Old tool says offline, new tool worksOld tool does not handle the A2S_INFO challenge
Works sometimes, times out under loadQuery rate limit reached, often during a flood
Player list empty, count correctGame hides A2S_PLAYER or a plugin blanks it
Works from your PC, not from a websiteThe site uses a different port or an old protocol

Work through it from outside: query the port you think is the query port, then the game port, then read the first lines of the server log for the port it actually bound. The log wins every argument.

FAQ#

Is the query port the same as the RCON port?

No. The query port answers anonymous status requests over UDP. RCON is an authenticated remote console, on Source games over TCP, and should not be exposed more widely than you need. RCON safely covers that.

Do I need the query port open if the server is private?

Players can join a private server by direct address without it. But Steam favourites, status bots and the in-game browser all use the query port, so closing it makes the server look offline everywhere. Most people leave it open and rely on a password.

Why does my query tool show a different player count than the game?

Bots are included in the player count in A2S_INFO and listed separately in the bot field. Some games count connecting players, others only fully spawned ones. A small difference is normal; a count stuck at zero points to a mod or a wrong port.

Can I use A2S to monitor uptime?

Yes, and it is a better check than a ping, because it proves the game process is answering rather than just the machine. Query every minute or two, alert after several consecutive failures rather than one, and remember that a reply does not prove players can join.

Does every Steam game support A2S?

Most dedicated servers built on Steam's game server API answer A2S, but not all, and some answer only part of it. Games that list through Epic, PlayFab or their own service often do not answer A2S at all. When in doubt, query it and see.


Comments

Completely anonymous: no account, no email, no cookie. We store the name you type, the text and the time - nothing else. Links are limited and markup is not rendered.

0/2000