Network events: checking what the client sends
A network event is a message a player’s game sends to your server (TriggerServerEvent). A cheat can send any event, with any argument, at any rate. The protection is on the server: Bastrel checks the call before your handler runs, and your handler only sees calls that passed.
You register an event through Bastrel instead of RegisterNetEvent + AddEventHandler:
-- fxmanifest.lua of YOUR resourcedependency 'bastrel'exports.bastrel:registerNetEvent('farm:harvest', { { 'string', maxLen = 32, pattern = '^[%w_]+$' }, -- pointId: the client only sends which point}, { rate = { max = 3, per = 10000 }, -- 3 calls per 10 s and per player acl = { jobs = { 'farmer' } }, context = { 'alive', 'notInVehicle' },}, function(src, pointId) local point = Points[pointId] if not point then return end exports.ox_inventory:AddItem(src, point.item, point.amount) -- the amount is decided by the serverend)- The handler gets
src(the player’s server id) as its first argument. Do not read the globalsourcein it. - Never take an amount, a price or an item from the client. Send an identifier and decide the rest on the server.
- Register your events when your resource starts. The exports of another resource are available after the first tick of the scheduler:
dependency 'bastrel'makes surebastrelis started first. Registering beforebastrelis ready is an error in your resource. registerNetEventanswerstrue, orfalse, reason(and logsnetevents.register_failed) when the schema or an option is wrong, the handler is missing, or the event name is already registered by Bastrel (one registration per name).- Names that belong to the game or to Bastrel are refused: the server events of the official reference (
playerDropped,playerConnecting,onResourceStart,entityCreating,weaponDamageEvent, …), the events Bastrel handles itself, and any name starting withbastrel:or__cfx_. - The handler can be any function. Your function reaches Bastrel as a function reference (a table, not a Lua
function, because it crosses a resource boundary). Bastrel accepts a Lua function, or a table whose metatable (asgetmetatablereturns it) has a__callthat is a Lua function; it refuses a number, a string, a table without such a__call, a__callthat is itself a callable table, and a protected metatable that is not a table (on purpose, to stay conservative). The error isthe handler must be a function or a function reference. - The handler is called under
pcall: an error it raises does not stop the other events. For a handler of Bastrel’s own resource it is logged (netevents.handler_failed). For a handler of YOUR resource (a function reference) the function runs in your resource: FiveM prints the error there, andnetevents.handler_failedis still logged but without the message (erroris"nil", observed in game). Read your own console for the error. - Restarts. If you
restartthe resource that registered an event, Bastrel releases it when that resource stops (netevents.released) and your resource registers it again when it starts:restart my_shopworks. The name stays reserved for that resource untilbastrelrestarts: while it is stopped, another resource that tries to register the same event is refused (false, reason, logged asnetevents.name_reserved), so a restart never opens a window for someone else to take your event. If yourestart bastrel, every registration is lost: register your events again whenbastrelstarts, withAddEventHandler('onResourceStart', function(resource) if resource == 'bastrel' then register() end end)(and once at your own start ifGetResourceState('bastrel') == 'started'). Until then your events are not protected by Bastrel and not registered with the game.
Ox_lib callbacks: registerCallback
Section titled “Ox_lib callbacks: registerCallback”exports.bastrel:registerCallback('farm:price', { { 'string', maxLen = 32 } }, nil, function(src, item) return Prices[item]end)Same schema, options and controls; it wraps lib.callback.register. A refused call returns nothing to the client. ox_lib must be installed and started before bastrel (it is a dependency of bastrel): its init.lua is loaded by Bastrel’s manifest.
What is checked, in this order
Section titled “What is checked, in this order”The cheapest first, so that a flood cannot use up the server:
- Source: a call that does not come from a player Bastrel tracks (for example a local
TriggerEvent) is ignored, without a detection. - Rate (only if you give
rate): a token bucket per player and per event. Beyond it:BAS-NE-002. There is no default rate: an event withoutrateis never cut, so a busy interface callback is safe. - Size of the whole payload (arguments, nesting, keys, string length, total bytes) against
netevents.limits, before anything is copied:BAS-NE-001. - Schema (types, bounds, patterns):
BAS-NE-001. - ACL (
acl):BAS-NE-003. - Context (
context):BAS-NE-004. - Client SDK (
BAS-NE-005): seesdk.md. It is a faint clue that never blocks a call, kept out of the score until you calibratesdkinconfig/whitelists.lua.
A refused call never reaches your handler. A function sent by a client in an argument (it arrives as a function reference, not as data) is refused as BAS-NE-001 (bad_value), so it kicks in enforce; Bastrel never indexes it, so it cannot cause an internal error that would hide the detection. The same goes for a function reference given where a schema or an option expects a table: registerNetEvent answers false, reason.
The schema
Section titled “The schema”A list with one entry per argument:
| Entry | Meaning |
|---|---|
{ 'string', minLen?, maxLen?, pattern? } |
pattern is a Lua pattern, matched anywhere: anchor it ('^...$'). |
{ 'number', min?, max?, integer? } |
Finite numbers only (NaN and infinity are refused). integer = true accepts 3 and 3.0, refuses 1.5; your handler always receives a Lua integer (3.0 is converted to 3). { 'integer', ... } is the same. |
{ 'boolean' } |
|
{ 'vector2' }, { 'vector3' }, { 'vector4' } |
A value whose Lua type is that vector (type(vector3(1, 2, 3)) is 'vector3' in FiveM’s Lua). Components must be finite. How a vector sent by a client arrives in a network event is not documented by FiveM and not yet captured: test your event with a real client before relying on it, or send the components as numbers. |
{ 'table', fields? = { key = entry }, items? = entry, maxItems? } |
fields: exactly these keys (optional ones may be absent), no other. items: a list whose elements all match. Neither: any table within the size limits. |
optional = true on any entry |
The argument may be nil. |
An extra argument (a trailing nil beyond the schema is not one: Lua cannot tell it from no argument), a function, a key that is neither a string nor a number, or a self-referencing table is refused. A schema with a mistake (unknown type, misspelt field, a pattern that does not compile) refuses the registration instead of silently accepting everything.
The options
Section titled “The options”| Option | Meaning |
|---|---|
rate = { max, per } |
At most max calls per per ms, per player and per event. |
acl |
All the keys you give must pass. jobs = { 'farmer' }: the player’s job is one of them. grades = 2: grade 2 or more (or { farmer = 2, baker = 1 }: the minimum for the job). groups = { 'crew' }: the player has one of these groups. ace = 'farm.use' (or a list): one of these ACEs is allowed. Job, grade and groups come from the framework adapter (QBX). An ACL on jobs, grades or groups needs a framework adapter: on a server without one, or while a player’s character is not loaded yet, such an event is refused (see below). ace does not need the adapter. If the game cannot answer the ACE check (unknown answer), the call is refused with no detection. |
context |
A list: 'alive', 'notInVehicle', 'notCuffed', { near = { coords = vector3(...), radius = 3.0 } } (the player’s ped is within radius of fixed coords), { bucket = 0 } (the routing bucket). |
limits |
Size limits for this event only, same keys as netevents.limits. |
requireSdk |
true by default (requireSdkDefault): the event is announced to the client SDK (sdk.md) and a call without SDK data is BAS-NE-005. false: never announced, never judged. |
onViolation |
'flag', 'drop' or 'ban', see below. |
When Bastrel cannot know something, it never accuses the player:
- ACL: the call is refused, with no detection and no score (the ACL fails closed, otherwise a player whose character is still loading would reach a restricted event). One warning line
netevents.control_unknownper event and check says so. Without a framework adapter, every event withjobs,gradesorgroupsis refused. - Context: the framework answer comes first (
aliveuses the framework, then the health of the ped only if the framework does not answer). When nothing answers, that check lets the call through (BAS-NE-004isunverified) and logs the same warning.
The detections
Section titled “The detections”| Code | Cause | Effect |
|---|---|---|
BAS-NE-001 |
Payload outside the size limits or the schema | Deterministic, medium: kick + staff alert in enforce (the matrix of config/modules.lua). |
BAS-NE-002 |
Rate exceeded | Heuristic: feeds the score. |
BAS-NE-003 |
ACL refused | Heuristic: feeds the score. |
BAS-NE-004 |
Context refused | Heuristic, unverified: logged only, no score and no alert, until the game reads it relies on are captured in game. The event is still refused. |
BAS-NE-005 |
SDK data missing or invalid, or a refused hello |
Heuristic, weight 2, tied to the sdk list: observe and out of the score until you calibrate it. Never blocks the call (sdk.md). |
BAS-NE-002 to BAS-NE-005 are sent to the scoring once per player, event and window (netevents.violationReportMs, 10 s by default); every refused call stays refused. So a player who clicks a rate-limited button twelve times in a second is refused twelve times and scored once. BAS-NE-001 is reported on every call.
Everything goes through the usual decision: nothing is applied in observe, a player with bastrel.bypass.netevents gets no sanction. A score alone never bans.
onViolation replaces the action of BAS-NE-001 for that event only: 'flag' = staff alert only, 'drop' = kick + alert, 'ban' = ban + alert. Without it, BAS-NE-001 follows the matrix (kick + alert). It only applies to BAS-NE-001; on BAS-NE-002 to 004 it is ignored (a warning netevents.onviolation_ignored is logged once per event). A heuristic never bans.
onViolation = 'ban' is available to every resource that registers an event. It only bans on BAS-NE-001, a schema violation of an event whose schema you wrote, but a schema that is too strict will ban honest players: start with 'flag' or 'drop', and test your schema with real clients first.
Your own detections: report
Section titled “Your own detections: report”-- config/modules.lua, block netevents: declare the code once, with its score weightownerCodes = { ['BAS-OWN-001'] = 15 },exports.bastrel:report(src, 'BAS-OWN-001', { pointId = pointId })- Only codes you declared in
netevents.ownerCodesare accepted (BAS-OWN-and three digits). They are always a heuristic of severitylow: they feed the score and its thresholds, never a ban on their own. Bastrel adds them to its own code table at startup, so logs and Discord treat them like its codes. - Every code shipped by Bastrel (
BAS-GE-001, …) is refused: otherwise a resource of yours, even a compromised one, could trigger a ban with it. - The third argument is kept in the evidence, with the name of the calling resource. A fourth argument
true(“deterministic”) is ignored, with one warning. reportanswerstrueorfalse. A refusal is logged (netevents.report_refused, once per resource and reason innetevents.logWindowMs).netevents.reportAllowedResources(a list of resource names) limits who may report; empty = any server resource.
Configuration
Section titled “Configuration”config/modules.lua, block netevents:
| Key | Default | Meaning |
|---|---|---|
limits |
maxArgs = 32, maxDepth = 6, maxKeys = 128, maxStringLen = 4096, maxTotalBytes = 65536 |
Size bounds of a payload. Generous: a payload past one of them is BAS-NE-001, which kicks, so raise them for an event that legitimately sends more (opts.limits). |
requireSdkDefault |
true |
Default of requireSdk. |
ownerCodes |
{} |
Your codes: ['BAS-OWN-001'] = weight. |
reportAllowedResources |
{} |
Who may call report. |
logWindowMs |
60000 |
A refusal of report and a failing handler are logged once per window. |
violationReportMs |
10000 |
BAS-NE-002 to BAS-NE-005 reach the scoring once per player, event and window (ms, 1000 to 3600000). |
An invalid value takes its default, with a warning; the valid ones are kept. The exception is reportAllowedResources: an invalid list refuses every resource (it never means “anyone”). The weights of BAS-NE-001 to 005 are in scoring.weights (20, 10, 15, 10, 2). The sdk block is described in sdk.md.
Limits to know
Section titled “Limits to know”- This protects your events. A cheat that calls your event with a valid payload still gets through: that is why you decide amounts, prices and items on the server and check distances and rights in the handler.
- The client SDK (
sdk.md) only slows down a cheat that creates its own runtime; it cannot stop code injected into a resource that is already running. - A local
TriggerEventof a registered event (from another server script) is meant to be ignored, because itssourceis not a tracked player, but the value ofsourcefor a local trigger is not documented: do not rely on it, and call your own functions directly instead.