“Kazakov 3” reverse engineering, part of the network: we create a local server

    Recently, in a conversation with colleagues, we discussed various games of the RTS genre , and I wondered why the release of the third “Cossacks” passed me by. A couple of minutes and one search query later I remembered - in addition to an extremely crude early release, the reincarnation of this classic strategy was distinguished by the impossibility of a multiplayer game without a permanent connection to the official server. Numerous requests from players to “add LAN” on forums of varying degrees of freshness hint that changes should not be expected.

    Well, if the mountain does not go to Mohammed ...

    TL; DR: Server, instructions for use and source code are available on GitHub .

    First steps

    In order to understand the general structure of the protocol, the first four packets that are eavesdropped between the game client and the official server are enough. So we have:

    The packet header always takes exactly 14 bytes and contains the payload size (1), the command code (2) and two player identifiers for packet addressing (3.4). Let's take a simple example - a private message in the chat of the gaming lobby:

    Here you can also see that the lines are preceded by their length (5). It is noteworthy that, depending on the specific command code, the data format is different and the size of the lines is indicated by one, then two, or even four bytes.

    Consider a public notification about the creation of a new game room:

    The header also begins with the size, command and sender (1,2,3), but the recipient identifier is missing (4). In this message, it means “send to everyone”, but for other teams it can also mean “to everyone in his room”. The first line (5) contains the name, password and information about the type of game and version of the client at the creator (host) of the room. To separate the values, the tab character \ t is used, it is 09h. Then follows a line with information about the room (6), which is required to display it in the list. It contains the status, the number of live players, computer opponents, closed slots and two more values. Here, the vertical bar plays the role of the separator. After it are followed by two constants of 4 bytes each (hereinafter referred to as a number of type Int ), then a string with the host name room maker computer (7).

    Anticipating questions arising from reading, I note that ...
    • Yes, passwords to private rooms are transmitted to all players on the server.
      (See also the article “ The Cossacks Have No Secrets ”)
    • No, the host name of the creator of the room is not required for the game, because all packets are transmitted through the server.

    Now let's move on to more interesting points.

    Translation difficulties

    Initially, I analyzed packets in Wireshark with tcp && data filters so that “empty” packets like ACK would not flash before my eyes . At some point, it turned out to me sideways: it turned out that Wireshark mistakenly accepts packets whose TCP payload starts with bytes 05h 00h for DCERPC protocol packets . In particular, this affected packets with a notification about a player entering the room, as they always contain exactly five bytes after the header. This leads to the fact that Wireshark marks the TCP load not as Data , but as [Malformed Packet: DCERPC] and hides the packet: The use of the newer tcp.payload filter would be correct in this case .

    . It displays all TCP packets with a payload, regardless of how Wireshark interprets this load.

    Serialization Evolution

    When reversing the network protocol, it was obvious that different people with different priorities worked on it at different times. Three types of lines passing variables can be distinguished:

    • compact strings
      The length of the string is indicated by one byte, the strings contain the names of variables, the fields are separated by vertical bars:ps=1337|pw=42|pg=12
    • intermediate strings
      The length of the string is indicated by two bytes (hereinafter referred to as a number of type Short ), the values ​​are separated by a tab, no names:"MyRoom"\t"secret"\t0AFFE
    • generous strings
      Special strings, which are actually an array of strings with alternating variable names and their values. A kind of associative array in one-dimensional form. The length of each (sic!) Of the lines in it is indicated by a number of type Int . A detailed description of the format can be found in the comments on the server source code.

      Numbers indicating lengths of lines are marked in red, yellow - separators.

    The last example is taken from a package that regulates the transit of the host role to one of the players when the creator of the room leaves the game. As I understand it, this functionality was further developed after the release of the game. Obviously, then the question of data transfer efficiency was no longer as acute as at the time of writing the network protocol in the original engine.

    Sloppy buffers

    At the end of many packets, the payload ends with four or six zero bytes. But in certain packages (in particular, with the command codes 0xc8 and 0x19d ), data sometimes pops up unexpectedly among them. I just could not understand where they came from and why they were needed, until in one of these packages I found in them a fragment of one of the messages in the lobby chat.

    Apparently, the official server does not always zero out the buffer into which the response packet is written before sending, and the remaining bytes may be lost in the closing bytes. Fortunately, in the game client itself this does not lead to errors. But something still says something about the pace of development and the level of quality control.

    You know less - a stronger server

    ... or " the worse the better ." After gaining a critical mass of knowledge about the protocol used, the amount of source code stopped growing and began to melt. The interfaces were simplified, and the data that the server was forced to save (for example, to transfer the state of the lobby to new arrivals), ceased to be analyzed and began to be stored as strings. It is not necessary to know the origin and destination of each byte in the packet; just understand how and to whom to redirect it.

    The server responds to most requests with data received from other clients earlier, or provokes the desired client with a special package and redirects the response to the requestor. Where possible, I omitted optional strings, replacing them with a null byte. First of all, this concerns the data on the rating of players, the number of points and victories.

    In some cases, too wide packet relaying even generated client-side errors. In particular, I noticed this on the “request-response” pair with the command codes 0x1b3 and 0x1b4 , duplicating information about the player’s points and the client’s system.

    Get in line

    Of particular difficulty for me was the aspect of launching and synchronizing the game. Having dealt with packet relaying at boot, I noticed that during the game all clients send packets with the 0x4b0 command code . At the same time, the number of the creator of the room is in one field of the identifier, and the second field is empty. But if you approach logically and understand this as “the client is everyone in the indicated room”, then the game will be out of sync.

    Instead, the server itself must monitor the source of the packet and check whether it is a host or an ordinary player. In the first case, the packet is sent to all players in the room except the host itself, in the latter - exclusivelyto the host. At the same time, a package with a game team, apparently, is added to the queue of what is happening and then sent back from the host, but in the context of all other events of the game. This guarantees the same order of execution for all players. The host sends out game packages constantly and at a certain tact, regardless of the number of events, and all the rest - only with actions from the player.

    By the way, that is why when changing the host during the game, a warning is displayed stating that your orders may be lost - if the old host did not manage to add them to the general queue before disconnecting, then the new host has no way to find out about these commands.

    Rudimentary LAN

    Contrary to public opinion, the client still has the functions necessary for a full game on the local network. When analyzing network traffic, I noticed that among the many TCP packets, the creator of the room also sends out notifications about the number of players and the status of the room via UDP .

    In addition, when disassembling the client, I came across a function that is called when exceptions occur and displays the error text. Going through her calls and analyzing the text transmitted to her, you can determine the real names of those procedures in which exceptions arise. A little surprised at the result, I played around with strings ,
    and here is what I saw:
    function LanPublicServerGetRegIDFrom
    function LanPublicServerGetRegIDTo
    function LanPublicServerGetRegMessage
    function LanPublicServerGetClientTeamByIndex
    function LanPublicServerGetClientTeamByClientID
    function LanPublicServerGetClientSpecByIndex
    function LanPublicServerGetClientSpecByClientID
    function LanPublicServerGetClientInfoToParserByIndex
    function LanPublicServerGetClientInfoToParserByClientID
    function LanPublicServerGetSessionInfoToParserByIndex
    function LanPublicServerGetSessionInfoToParserByClientID
    function LanPublicServerGetClientsCount
    function LanPublicServerGetSessionsCount
    function LanPublicServerGetClientIndexByClientID
    function LanPublicServerGetClientIndexByClientNick
    function LanPublicServerGetSessionIndexByClientID
    function LanPublicServerProfScore
    function LanPublicServerProfCountry
    function LanPublicServerProfGamesPlayed
    function LanPublicServerProfGamesWin
    function LanPublicServerProfLastGameTime
    function LanPublicServerProfInfo
    function LanMyInfoHost
    function LanMyInfoIP
    function LanMyInfoID
    function LanMyInfoSpec
    function LanMyInfoName
    function LanMyInfoPlayer
    function LanGetServerInfoToParser
    function LanIpToString
    function LanIpToInt
    function LanGetClientsCount
    function LanGetClientIDByIndex
    function LanGetClientHostByIndex
    function LanGetClientNameByIndex
    function LanGetClientSpecByIndex
    function LanGetClientIndexByID
    function LanGetClientPlayerNameByIndex
    function LanSelectParser
    function LanGetParserID
    function LanGetSendDataThreadCount
    function LanGetSendDataThreadEnabled
    function LanGetNoDelayOption
    function LanGetOptimizedPackage
    function LanGetOptimizedPackageDef

    There are two options: either the developers have such humor, or they universally used the acronym LAN for everything related to multiplayer games.

    C ++, Asio and unscathed legs

    Since I initially set myself the goal of writing a cross-platform server and expanding my knowledge of network programming, for the implementation I chose the C ++ language and the Asio library . The latter also allowed me to abandon multithreading and related features of data access in favor of asynchrony and simpler code. As a basis, I took the source code of one of the examples in the library repository .

    The most interesting aspect of development for me was the problem of the availability of the data buffer during asynchronous sending of packets. At the same time, I tried to minimize the number of allocations and copying data in memory. In addition to everything, the server should have a rather large buffer for receiving packets, as the size of the TCP payload when transmitting map data before the start of the game can exceed 800 kilobytes.

    As a result, I implemented the process of reading, creating and sending packages as follows:

    1. When connecting a new client, the server creates an object of the Session class containing a sufficiently large (1 MiB) buffer of the std :: vector type( hereinafter - Buffer ).
    2. After the asynchronous read operation is completed, this address is transferred to this buffer to the main packet processing function. The next read operation will be initiated only after the completion of this function, guaranteeing the safety of data in the buffer for processing time.
    3. At the beginning of processing, an object of the Packet class is created , which provides an interface for reading and serializing data. Through it, the server response is written all in the same buffer of the Session object , which is assigned to the sender of the packet.
    4. After all operations on the package are completed, the send function allocates a buffer using std :: make_shared. At the same time, the iterator of the same buffer in Session is passed to the Buffer constructor , given the exact size of the response packet ( Packet monitors this during recording ). Those. in one operation, we first allocate enough memory for the packet and the control block of the pointer (and at a time ), then copy exactly the number of bytes from the large buffer that should be sent to it.
    5. The new buffer is passed using the resulting pointer of type std :: shared_ptr( hereinafter referred to as BufPtr ) to the Session objects of all those clients to whom the packet should be sent. There it is placed in local queues like std :: deque. Each copy of the index that is in the queue of any of the clients increases the reference counter. After that, packet processing ends, and the initial Session buffer is ready to receive the next packet.
    6. After the asynchronous recording (sending) operation of the packet is completed, the pointer is deleted from the local Session queue of the destination client, lowering the reference counter. As soon as the packet is sent to all clients in which it was in the queue, the counter will be reset to zero and the smart pointer will free up memory on its own.

    No matter how you judge, the addition of lambda expressions, various containers and smart pointers to the C ++ language standard significantly reduced the complexity of designing such systems; when writing the server, not a single leg was shot.

    Shutter speed test

    Having finished work on the server and having tested the basic functions, I decided to check the stability of the server and synchronization after the host transit after a long time. To do this, I created a room on the local network with three players and five complex AI . For the test, I chose the maximum parameters regarding the map size, number of deposits, population and non-aggression time. I started the game and after a couple of minutes I stopped the game process on the host computer in the task manager to simulate an unexpected shutdown. Then the two remaining clients were left to themselves all night.

    Experienced players have probably already guessed what happened next. In short, the server passed the test, but the client did not.The next day, the screen greeted me with a game timer that froze for about eight hours, as well as several error messages, among which was the familiar Out of memory. Everything is logical, the two remaining AIs divided the map in half and fought with thousands of armies. The game process took about 3.5 GB of RAM and ran into its 32-bit boundaries . The server, in turn, continued to operate on its 11 MB of RAM.


    I hope you were interested in following the process of copying the black box of the official server. I also hope that the developers of the game will take pity on the players and finally add the possibility of a multi-player game on the local network, with blackjack and fast UDP, and without the need to have a low ping to Hetzner . The last argument, by the way, flirted with new colors in view of recent events .

    If you have questions or suggestions regarding the reverse or source code of the server - welcome to comment. See you soon!

    Also popular now: