Pop quiz! You're implementing a shop in a server authoritative multiplayer game. There's multiple items the player can buy, each with different prices. The player selects an item to purchase:
As the player selects their item, a network message goes out from the client to the server. What data do you include in that message?
The most straightforward thing you could do is:
Simple enough! Receive the data on the server, add ItemID to PlayerID's inventory, lower their gold by Price. Easy enough to implement.
Now, of course, we shouldn't include the Price in the network message. If we do, the client could simply send us a tampered request like:
Free brown boots! Perhaps they can even send in a negative Price value.
That's an even better deal, you're paying them to take the brown boots off your hands. Now that's a business plan!
So, let's get rid of the price. On the server, we will just use our Item ID to retrieve the full data for the item, including its price. A little bit more work, but we've removed a giant exploit.
However, the sneaky client can still perform an exploit. Free items aren't a thing anymore, but player 3 can now send this message:
Any player can get any other player to purchase any item! Just tamper the Player ID, and you can get some unlucky schmuck on the enemy team to buy several useless brown boots, depleting all his gold in the process.
So, well, we scratch that too. But if our network message only has the itemID, how do we know which player to give the item to? Well, the answer is more work on our part!
When players connect to the game, we will need to associate each connection with a corresponding player. Then, when we receive our purchase message, we determine the PlayerID of the sender based on what connection the message came from. This is a non-trivial identity/authentication problem, but you'll most likely be using a library that solves it for you. Mirror for Unity is what I use.
Player identity determined server-side from connection context.
Well, that's even more complicated than what we started from, but at least now we should be good. We have a minimal network message. Surely this is foolproof. Well... Almost, but not quite there. Our network message is indeed as minimal as it can get, there's nothing else we can remove. But, while we do need some itemID from the client, we can't really trust that it is a correct itemID. The client tries these messages:
Uh oh, our item ID's only go up to 512. We take the itemID, try to retrieve our corresponding item. It doesn't exist. Our server crashes.
We add a check to make sure the provided ID is not out of bounds.
Client sends:
Finally a valid item! But, turns out the client has 0 gold. We have a check on the client graying out the button for items that the player can't afford, but Bob is obviously not stopped by that. We never added a check on the server to check if the player actually has enough gold, so Bob gets the item and has his gold value set to -450. Being broke doesn't matter if you can go into infinite debt.
We should also probably check if the player has free item slots, if the player is in range of a shop, every single gameplay condition. Once on the client to set UI state. Once on the server to verify against cheaters.
So we patch all that as well. Minimal data. Valid ID. Gameplay conditions met. Phew.
Client sends:
And then does it again. And again. One hundred thousand times per second. Our server crashes. We implement rate limiting to disconnect spamming players.
Now, we have to repeat the same process for every single message the client sends to the server. There's no silver bullet, and you'll need to consider each case individually, but there are some rules of thumb that we've applied here:
Take exactly as much data as you need from the client and no more. In practice, this should always consist of data communicating 'intention'.
Check the validity of all the data you do receive. Do you have enough gold to buy this item? Is the position you want to walk to actually somewhere you can navigate to? Is the ability you want to use off cooldown, and do you have enough mana?
In practice, this almost always means you have to do the validity check twice: Once on the client to set UI state, and once on the server to prevent tampering from cheaters.
A useful trick for the second point is to design your UI and logic with a debug always-on setting in mind. When enabled, you should have all of your buttons and inputs be allowed, at all times. This lets you easily simulate what a cheating client would do, and is helpful in finding exploits you've missed.
In general, you should aim to design your own cheats where possible. It doesn't take too much work to hard-code a few tampered requests you can send whenever you want.
So that's that for data we receive from the client. Unfortunately, that's just half the work. We also have to worry about what we are sending to the client.
I cover that in part 2.
Links: