How to check if a player is spamming a remote too fast?

I can give you a simple, concrete example, as that wiki article is actually kind of verbose.

Suppose that your game lets a player swing their sword as fast as 3 times per second, and that attacking faster than this is only possible with an exploit. You might first think to have an attack counter for each player, that is checked and reset once per second on the server. The trouble with this is that it’s too simple and doesn’t account for variability in internet latency: I could legitimately attack a player at max attack speed for 2 seconds, a total of 6 attacks each 1/3 of a second apart (on my client). But, due to unequal transit times across the internet for my events, or a back up of event processing on the server, the server might see this as 2 attacks in the first second, and 4 attacks in the next 1-second interval. 4 attacks per second is above the max attack speed, but events from my client were valid!

So, what you do instead is something like this: Each attack you get from my client increase my attack counter. Once per second, you subtract up to 3 from my counter. Then after this subtraction, you compare the value of my attack counter against some threshold, which is a bit of an arbitrary safety margin. Suppose the threshold in this case is also set to 3 (which will tolerate a transient of up to 2x normal attack rate). What happens is my first 2 attacks make my counter 2. On the next check 1 second later, you try to subtract up to 3 from this, so my counter is now 0 (counter clamps at min of 0). 0 is less than the threshold of 3, so I’m still fine. On the next check, you process my 4 attacks, setting my counter to 4. The following second (during which I made no further attacks), you again start by subtracting 3, reducing my counter to 1. 1 is less than the threshold, so I’m still within the allowable tolerances.

Now, suppose I exploit and start sending 5 attacks per second. Well, you can see that adding 5 but subtracting 3 on each check will quickly overflow the bucket. My counter will go like this: 5-3=2(OK) 2+5-3=4(overflow!)

Basically, buffering the input in this manner makes it tolerant of events sometimes bunching up, yet still possible to validate the average rate of events that do naturally happen in bursts (like sword attacks). The tuning parameters for this kind of system are the bucket size (the threshold), and max allowed rate, and how often you check. Realistically, exploiters tend to just spam events at many times the normal rates, so you can be pretty generous with bucket size.

82 Likes

You can also upgrade the leaky buckets by instead of decrementing every so often, keep track of the last time the bucket was decremented. When a request comes in, decrement the bucket by the time since the last decrement divided by the time per a unit of leakage. This can be done with floating point values (like Lua numbers) or integers and there isn’t a need to perform rounding operations.

This should be very cheap too, nice suggestion Emily!

8 Likes

A simple way is to have a server sided dictionary, containing booleans stored by online user ids.

local debounces = {}

On every remote call, check if the boolean exists / is true. if debounces[userId] then…
if it does not exist or is false, set it to true, and set it to false after your function is done.

2 Likes

The drawback with this kind of system is that it has no tolerance for varying latency, it uses server arrival times, but the timings of events is not preserved through transmission over the internet. Valid event timings from a client can easily be rejected, which is frustrating for the player, especially if the client is doing any kind of prediction (like visually firing a shot, for which a projectile is then never created on the server).

In most cases, you want to make sure the average rate of client events is within sane limits, but in a way that tolerates variation in the arrival times of individual events. It’s not trusting the client completely, just giving the system enough margin of error to not start falsely rejecting valid input because of variations in ping.

2 Likes

Can’t you use os.time() difference checks instead then?

Yeah, and you can use tick() as well. This wouldn’t be as efficient though, because you’d need to implement a double sided queue. As requests came in and were accepted, you’d push a timestamp onto the top of the queue. To check if too many requests were coming in, you’d pop the oldest requests off the bottom of the queue until you reached one that was within a memory threshold (say, 10 seconds). You’d then compare the current number of requests in the queue to determine if there have been more than a desired amount in 10 seconds.

1 Like

I wouldn’t recommend it. If you trust client timestamps, you just give other types of exploiters (cheaters) something very obvious to tamper with.

For detecting true “spamming” of remotes, average rates at the server end are all you really care about, this isn’t the same use case as detecting players who are doing things like using skills before they go off cooldown, exploiters who are spamming remotes are generally not trying to cheat, they are trying to overload a remote that does something expensive (like cloning models from storage) in order to crash the server.

1 Like

You can use os.time() on the server :stuck_out_tongue: and just have a hashed table with last uses stored by userid.

I don’t see what use this would be on its own. What would you do with this data?

You could create a table of tick() for each player, and each time you fire set a variable like

local players = {}
newtick = tick()
if newtick > players[p]  + 1 then
print("All good")
end

This is just an example, so if it’s not enough time you can just change it up and make it better.

EDIT: Also players[p] in this situation would be the old tick() and after if it prints all good set players[p] to the new tick()

This is just another flavor of “time since last event” check. I’ve explained above why this isn’t a good general-purpose approach for flood control.

os.time() can be called on the server. So the argument of trusting the client is invalid in this case. That is why I don’t really understand why you don’t think it’s a simple way to prevent the spamming of remotes.

I know os.time() can be called on the server, I just don’t see how it’s useful. Let me illustrate. I’m playing your game, and I click 4 times to shoot a weapon, say each ~0.2 seconds apart. Each time I click, the game client fires a RemoteEvent, indicating that I fired a shot. Now, many miles away (possibly thousands), my RemoteEvents arrive at the game server, all at the same time and get processed (OnServerEvent) all on the same tick, with the same os.time stamp (they don’t need to be exact, but suppose the differences are way less than the original 0.2 seconds). Am I now a spammer with your system? I would not be with my system, because it is very forgiving of individual server arrival time differences.

1 Like

Yeah I guess you are right about that, but it’s a simple way to do it.
I’ll read your way of doing it because I haven’t had time to read it completely yet.

From what I understand and from personal experience, imagine if you have more than 1 player. If you have a denounce in that Event, and both players fire at the same time; It will only swing for one of the players and then wait(.5). As far as the solution that I’m trying to figure out, I simply have a Check for each time a player clicks every second. If clicked, Check = true. Waits(1) Check = false. Of course, this is server-side.

Other RemoteEvent calls do not have to wait for previous calls to finish in order to spawn up.

1 Like

This seems like a good method of keeping traffic under control.

But what if I were to have a game with multiple cooldowns for multiple event-based actions such as the sword swinging and an action such as a sword skill that is longer and is expected to last 5 seconds, with a 3 second cooldown after it is used. Is there a way to verify that the player is not doing anything else while the skill is in use and to ensure the skill isn’t used for another 3 seconds once on cooldown with the leaky bucket algorithm?

You stated earlier that you should keep each event in its own “leaky bucket”, but with this approach what if the exploiters were to use the sword attacks and the skill all at once? In this case, both buckets would see it as normal traffic because the buckets don’t account for each other and the time the player has spent doing their accumulative actions.

You can keep them seperate while having some sort of “tether” to make sure they’re not spamming multiple things at once

Specifically, what would this “tether” look like in pseudo code and/or code?

I fail to come up with a method that can validate that no other action is in use without something like a Boolean value on the server and we already know that that wouldn’t work due to latency.

The specific example you outlined is something you’d handle in game code, a layer up from the remote event processing. You can still have seperate leaky buckets for each event type, but you have to also process input in the order you get it, and if something like the 3-second skill is meant to suppress normal basic attacks, you don’t change code at the event processing level for that, you just store the skill end time associated with the player and invalidate or defer the mutually-exclusive attacks during that cooldown window (in server time).

It’s a game design question as to whether you invalidate the attacks (completely disregard the events) or defer them (queue them up to be processed at maximum rate as soon as the skill ends). You can also do both–throw away attacks during most of the skill cooldown, but have a “fuzzy” tolerance for attacks that arrive right when the cooldown is about to expire. You could choose to only queue up an attack that is within some window of time around when the skill is ending, to allow for the possibility that the player saw the skill end before the server cooldown was quite expired.

One common way of doing this is to make something with, say a 3-second cooldown, not refresh the UI on the client for 3 seconds, but on the server the cooldown timer actually gets set to something like 3 seconds minus half the player’s average round-trip ping time. Or even a fixed-size allowance. It’s only exploitable to the degree you let it be with your choice of tolerance time.

1 Like