I’ve now open sourced an implementation of one of my anticheats for use in my game Hexolus!
You can find more info and the repo links here: Hexolus' Server-sided Anticheat
This is a super long thread so read it at your own pace.
How you should secure your game
You may have seen the “never trust the client” phrase tossed around if you’ve read or created scripting support threads about remotes or game security. I’m going to explain this phrase and give some useful information about remote and client security as well as give some information about effective anticheats.
Never trust the client?
Lua and bytecode
Most developers when they first start learning about game security will ask, “Why can’t exploiters just be stopped with a script on the client?” Well think about it like this. The server has to send some kind of code to the client so that the client can run this code. That means this code must be stored somewhere for the client to use it. And that code is stored in the client’s memory. This “code” isn’t like the lua code that you write though. It’s a version of the lua code called “bytecode” which the client can interpret. When you write your game’s lua code it will be compiled when the game starts and each LocalScript’s (and ModuleScript’s!) bytecode will be sent to the client.
Decompilers
This bytecode and the values and functions the bytecode accesses can be changed at any time. The fact that this code is stored in memory and an exploiter has access to it also means it can be “decompiled.” A decompiler on a basic level just tries to create lua code which produces the same bytecode when compiled. That means any information which is not included, like local variable names and comments also cannot be decompiled. This absent information would make the decompiled code much easier for an exploiter to read. Keep in mind, since the bytecode for server scripts (and module scripts located in ServerStorage and ServerScriptService) are not sent to the client, they can’t be decompiled!
Never trust the client!
An exploiter has access to the client side code in your game and can change all of it’s functionality, whether that be editing the script somehow, or changing the values and functions it uses. That means any client side checks you place on the client can just be removed by an exploiter and the values it checks can also be changed! Any functions you call on an instance or from the global libraries like math and table can be changed by the exploiter without you ever knowing. That means they can even intercept the data that your client receives from or sends to the server. All remote traffic from and to your client is visible to the exploiter!
Networking your game
What code should you run on the client?
The client should always “pretend” like it’s right. You can think of the client like a mini version of the server that the player has control over. You want the client to mimic your server code by itself and then notify the server that it performed the action. The server must verify that this action is allowed using the same code the client does.
Sometimes you need to resync the client such as for placing an invalid object. If the client places an item they don’t have they should see it before the server tells them to remove it. This is a sign that you’re doing it right even if it seems like that’s wrong. For example, input events should be handled on the client and when an action is done on the client, like an attack, the client should ask the server to also perform the attack action. The server will verify and clamp the request before it performs it.
What code should you run on the server?
Obviously any server-side objects should run code on the server. In this case the server is like the client without any user input. The server is also the “authority” meaning anything the server says to other clients should always be your definition of correct. You always want to avoid a client making something invalid on the server.
The server should always verify the client’s requests. This is usually as easy as simply sharing some server and client code and having the server mimick what it thinks the client should be doing.
Here’s some pseudo code for example:
-- Server
local function onAttack(player, target)
if target.Team ~= player.Team and inRange(player, target) then
attack(target)
end
end
-- Client
local function onAttack(target)
playAnimation()
if target.Team ~= localPlayer.Team and inRange(localPlayer, target) then
fireServer(target)
end
end
Don’t send too much data!
You should limit yourself to one basic remote request per frame for something like ping. Don’t send a lot of data per frame! If you are sending a complex table which needs to be updated you should send the initial table once and then update each value in the table individually only when the value changes! You always want to make sure you aren’t sending too much information to the client since this can slowly increase a client’s ping to several seconds! Don’t worry about sending too much data once as long as it means you aren’t sending it anymore after. Treat all of your remote requests like they are one big request.
Example:
-- Good
SyncRemote:FireClient("syncSettings", settings)
local function syncHandler(self, index, value)
SyncRemote:FireClient("syncSetting", index, value) -- We only send one property at a time and only when it changes! Much less perceived server lag!
end
local settings = setmetatable({}, {__index = settings, __newindex = syncHandler})
-- Bad
local RunService = game:GetService("RunService")
RunService.Heartbeat:Connect(function()
SyncRemote:FireClient("syncSettings", settings) -- Lots of information might be sent very quickly!
end)
Anticheats
Types of anticheats
Personally I classify anticheats into two main categories. Passive anticheats, and aggressive anticheats. Which one you choose is up to your preferences.
Passive anticheats
A passive anticheat is an anticheat which prevents a player from doing unwanted behaviors. This is my personal preference because it is usually more accurate and results in an overall better user experience if done correctly. These anticheats are hard if you’re a beginner.
Examples of a passive style anticheat:
If a player moves too fast or teleports without the server move them back to their previous position. (I usually just set their previous position when I need to teleport them on the server)
If a player is floating move them to the ground.
If a player noclips, move them back to their previous position.
-- No teleporting or speeding from the server side!
local leeway = 2 -- How many studs of leeway can the player have before they are stopped? Low values = more rubber banding during lag!
local player = aPlayer
local RunService = game:GetService("RunService")
local lastPosition
RunService.Stepped:Connect(function(deltaTime)
local character = player.Character
local humanoid = character:FindFirstChildOfClass("Humanoid")
if character and character.PrimaryPart and humanoid then
local maxSpeed = math.max(humanoid.WalkSpeed, humanoid.JumpPower) + leeway
character.PrimaryPart.Velocity = character.PrimaryPart.Velocity.Unit * (math.min(maxSpeed, character.PrimaryPart.Velocity.Magnitude)) -- Limit their velocity!
local currentPosition = character:GetPrimaryPartCFrame()
if lastPosition then
local deltaPosition = currentPosition.p-lastPosition.p
if deltaPosition > character.PrimaryPart.Velocity*deltaTime + leeway then -- Check if they are moving faster than their velocity!
character:SetPrimaryPartCFrame(lastPosition) -- Reset their position!
end
end
lastPosition = currentPosition
else
lastPosition = nil
end
end)
Aggressive anticheats
Aggressive anticheats punish the player for misbehaving. For example, if the player makes an unauthorized request that they can’t make on their own such as using another player’s items/structures, or attacking with a weapon they don’t have the player can be kicked or banned. This can also include detecting cheating.
Permanently banning a player is a huge risk for not a lot of effect. If a player is exploiting they are most likely using an alternate account and will just create a new one.
Honeypots
Honeypots are used to trick exploiters into firing a fake remote. For example, if your game has currency you can have a fake remote called “AddMoney”. Under normal circumstances having a real remote like this is an extremely bad idea. But it happens and that’s reason to turn it into a Honeypot! When the remote is fired you know that it was fired by an exploiter.
Other anticheat information
When developing anticheats you should remember that you can have false positives. You should always make sure you won’t invalidly punish a player even if it means reducing the strength of the anticheat. You can also combine both anticheats to get the best of both worlds. One example of this might literally be using both forms of anticheat, or it could be making it annoying for the player to play if they’re cheating without effecting them negatively. For example, instead of banning or kicking the player you can temporarily remove their ability to purchase items or interact with certain things. You can even disguise this as a glitch such as giving them a fake item named “Invalid Item” or something and maybe they’ll complain . Make it fun!