aVoidPointer

Can you actually use STEAMGAMESERVER_QUERY_PORT_SHARED together with SteamNetworkingSockets?

Disclaimer: This post discusses the Steam SDK API. However, it only mentions methods and functions which are available in the publicly listed API documentation at https://partner.steamgames.com/doc/api/ISteamGameServer. This documentation is available without signing an NDA, and there are also already multiple public dumps of the Steam SDK headers.

This post therefore does not reveal any new NDA'ed information that was previously unavailable. My intention is not to break any NDA by posting this, and it's being done in good faith to help people get started with Steam SDK development.


Introduction

One of the more mysterious options that one can use with SteamGameServer is STEAMGAMESERVER_QUERY_PORT_SHARED (previously known as MASTERSERVERUPDATERPORT_USEGAMESOCKETSHARE). The theory behind it is simple enough. By specifying STEAMGAMESERVER_QUERY_PORT_SHARED as value for the usQueryPort parameter of SteamGameServer_Init, we can instruct Steam to use only one port for communication.

The idea is that both server queries and regular game traffic can use the same port and socket, thus requiring one less port to be opened in the firewall. There's also another very important reason why one might want to do this, and it will be revealed in the last part of this blog series.

When using STEAMGAMESERVER_QUERY_PORT_SHARED, Steam is not going to set up a socket to use for server queries. Instead, it will push the responsibility onto the user of the API who then needs to separate incoming traffic on the game socket and delegate any server query packets to Steam using the ISteamGameServer::HandleIncomingPacket method, as well as sending any replies using ISteamGameServer::GetNextOutgoingPacket.

Server query traffic can easily be identified by the first 4 bytes which should all be 0xff. The entire protocol is documented here and multiple implementations exist. Hypothetically one does not even need to use ISteamGameServer::HandleIncomingPacket and ISteamGameSever::GetNextOutgoingPacket, but could rather use any of the implementations listed there, or implement the protocol themselves. However, if we are already using the SteamGameServer API, it makes sense for us to use it.

Some pseudo code for using STEAMGAMESERVER_QUERY_PORT_SHARED could look like this:

auto socket = setup_and_bind_socket();

while (true)
{
    auto packet = next_packet(socket);

    if (*reinterpret_cast<uint32_t*>(packet) == 0xffffffff)
    {
        // Handle Steam server queries
        SteamGameServer()->HandleIncomingPacket(packet);

        while (auto packet = SteamGameServer()->GetNextOutgoingPacket(...))
        {
            send_packet(packet);
        }
    }
    else
    {
        // Handle regular game traffic
    }
}

Once again, looks easy enough, right? However - if you are using Steam, it's very likely that you are using the excellent Steam Networking Sockets library. And if you are, things are not quite as easy.

The problem

If you tried to do this, you would quickly realize that there is a problem. With Steam Networking Sockets, you set up a listen socket using CreateListenSocketIP. This is going to create a regular UDP socket, bound to a port and address. The issue is that you lose all control over this socket. The only thing returned to you is an opaque handle that you can use to close the socket. That's it. There is no way to get access to the underlying socket.

This would not be a problem usually, but for implementing our pseudo code, it creates an interesting dilemma. Steam Networking Sockets takes full control over the socket, and only delegates control to us when a connection has been established using a series of handshakes. We therefore have no way to separate the traffic.

One might think "Well, this is Steam after all. Maybe it does the separation internally already, dispatching the server queries to SteamGameServer for us automatically?", but no, unfortunately that is not the case. Steam Networking Sockets actually has an open source ditto, GameNetworkingSockets that is almost 1:1 equivalent to Steam Networking Sockets. We can therefore look at its source to understand the design of the protocol.

Looking at src/steamnetworkingsockets/clientlib/steamnetworkingsockets_udp.cpp:197 we can find the following:

if ( *(uint32*)pPkt == 0xffffffff )
{
    // Source engine connectionless packet (LAN discovery, etc).
    // Just ignore it, and don't even spew.
}

Looks very similar to our pseudo code, doesn't it? Except for the fact that it's not handled. It's simply ignored and discarded. You might think that maybe the open source GameNetworkingSockets differs from the Steam Networking Sockets implementation. Unfortunately, that is also not the case. I have verified that this particular part of the implementation is 1:1 equivalent for the Steam Networking Sockets implementation that ships with the Steam client.

This means that, as soon as we set up a listen socket with Steam Networking Sockets, we're screwed. It's going to become a black hole that swallows all server queries, and we cannot do anything about it. Or can we?

Hooking to the rescue

There is actually something we can do. Looking at the GameNetworkingSockets source again, we can see that DrainSocket uses recvmsg to read data from the socket set up by CreateListenSocketIP. Hooking this function will therefore allow us to handle the data before Steam Networking Sockets does, and because Steam Networking Sockets will just ignore the server query packets anyway, we can handle them as we see fit without having to resort to hacks to prevent Steam from reacting to the same data.

I recommend the excellent rcmp library (https://github.com/Smertig/rcmp) for this purpose. It works for both Windows and Linux for x86 and x86_64, and it's MIT licensed.

Using sockpp (https://github.com/fpagliughi/sockpp) to simplify some of the sockaddr conversions, here is a working hook implementation for recvmsg:

rcmp::hook_function<&recvmsg>([](auto original, int fd, msghdr* message, int flags) {
	auto result = original(fd, message, flags);

	auto packet = message->msg_iov[0];

	if (packet.iov_len < 5 || *reinterpret_cast<uint32_t*>(packet.iov_base) != 0xffffffff)
	{
		return result;
	}

	sockpp::sock_address_any from(reinterpret_cast<sockaddr*>(message->msg_name), message->msg_namelen);

	if (from.family() != AF_INET)
	{
		return result;
	}

	sockpp::inet_address from_inet(from);

	if (!SteamGameServer()->HandleIncomingPacket(packet.iov_base, packet.iov_len, from_inet.address(), from_inet.port()))
	{
		return result;
	}

	char buffer[1024];

	uint32_t address;
	uint16_t port;

	while (auto size = SteamGameServer()->GetNextOutgoingPacket(buffer, sizeof(buffer), &address, &port))
	{
		sockpp::inet_address to(address, port);

		sendto(fd, buffer, size, 0, to.sockaddr_ptr(), to.size());
	}

	return result;
});

Keep in mind that if you wanted to actually do this for Windows, you would have to hook WSARecvMsg instead, as that is used in place of recvmsg on that platform.

After installing this hook, you should be able to initialize SteamGameServer with STEAMGAMESERVER_QUERY_PORT_SHARED, and set up a Steam Networking Sockets listen socket for your game port. Server queries will just work™ transparently through this hook.