Compare commits
4 Commits
2bafe12235
...
e50a08e5d3
Author | SHA1 | Date | |
---|---|---|---|
|
e50a08e5d3 | ||
|
fe39979ad2 | ||
2942ce1b50 | |||
0c4039dd1c |
29
README.md
29
README.md
@ -24,11 +24,14 @@ Repository : https://git.minetest.io/minenux/minetest-mod-governing
|
|||||||
|
|
||||||
#### Configurations
|
#### Configurations
|
||||||
|
|
||||||
| config param | value | req | default | example |
|
| config param | type | value | req | default/min/mx | example or description |
|
||||||
| --------------------- | ---------- | --- | -------- | -------------------------------- |
|
| --------------------- | ------ | ---------- | --- | --------------- | -------------------------------- |
|
||||||
| secure.http_mods | governing | yes | none set | geoip,governing |
|
| secure.http_mods | string | governing | yes | none set | geoip,governing |
|
||||||
| secure.trusted_mods | governing | yes | none set | auth_rx,governing |
|
| secure.trusted_mods | string | governing | yes | none set | auth_rx,governing |
|
||||||
| governing.checkapikey | `<string>` | yes | none set | 177cd4797a4949c4b1114d2e4ab9af11 |
|
| governing.checkapikey | string | `<string>` | yes | none set | 177cd4797a4949c4b1114d2e4ab9af11 |
|
||||||
|
| anticheat.timestep | int | 15 | no | 15 / 10 / 300 | How many time will run checks in seconds |
|
||||||
|
| anticheat.timeagain | int | 15 | no | 15 / 10 / 300 | How many seconds checks again (to compare) on a suspected player |
|
||||||
|
| anticheat.moderators | string | admin | yes | admin,singleplayer | Comma separated list name players that can watch and check stats |
|
||||||
|
|
||||||
The `governing.checkapikey` string is requested by registering a free (or paid)
|
The `governing.checkapikey` string is requested by registering a free (or paid)
|
||||||
account at https://vpnapi.io/ and **if not configured, will be used a simple geoip request**.
|
account at https://vpnapi.io/ and **if not configured, will be used a simple geoip request**.
|
||||||
@ -36,6 +39,8 @@ account at https://vpnapi.io/ and **if not configured, will be used a simple geo
|
|||||||
1. its powered by [VPNapi.io IP VPN & Proxy Detection API](https://vpnapi.io/api-documentation)
|
1. its powered by [VPNapi.io IP VPN & Proxy Detection API](https://vpnapi.io/api-documentation)
|
||||||
2. but if apykey is not set, its powered by [IP Location Finder by KeyCDN](https://tools.keycdn.com/geo)
|
2. but if apykey is not set, its powered by [IP Location Finder by KeyCDN](https://tools.keycdn.com/geo)
|
||||||
|
|
||||||
|
> Warning: anticheat its a WIP from rnd's anticheat mod with beowulf mod fusioned
|
||||||
|
|
||||||
#### Commands
|
#### Commands
|
||||||
|
|
||||||
| command & format | permission | description function | observations |
|
| command & format | permission | description function | observations |
|
||||||
@ -43,15 +48,20 @@ account at https://vpnapi.io/ and **if not configured, will be used a simple geo
|
|||||||
| `/killme` | interact | kill yourselft | no matter if killme mod is present or not |
|
| `/killme` | interact | kill yourselft | no matter if killme mod is present or not |
|
||||||
| `/govip <playername>` | governing | complete ip player info | Will require keyapi configuration set |
|
| `/govip <playername>` | governing | complete ip player info | Will require keyapi configuration set |
|
||||||
| `/geoip <playername>` | geoip | simple ip player info | no matter if geoip mod is present or not |
|
| `/geoip <playername>` | geoip | simple ip player info | no matter if geoip mod is present or not |
|
||||||
|
| `/cstats` | moderator | to see latest detected cheater | its not a privilegie, use config settings |
|
||||||
|
| `/cdebug` | moderator | to see even suspected cheats to be verified later | its not a privilegie, use config settings |
|
||||||
|
|
||||||
#### privs
|
#### privs
|
||||||
|
|
||||||
* `geoip` can make a geoip query, no matter if geoip mod is present or missing
|
* `geoip` can make a geoip query, no matter if geoip mod is present or missing
|
||||||
* `governing` can run administration command like ip check
|
* `governing` can run administration command like ip check
|
||||||
|
|
||||||
|
> Warning: currently moderator its not a privs.. its a list of players names, its a WIP from rnd's anticheat mod with beowulf mod fusioned
|
||||||
|
|
||||||
#### Api
|
#### Api
|
||||||
|
|
||||||
|
Currently only geoip functions are exposed, we later exposed antcheat also
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
-- lookup command
|
-- lookup command
|
||||||
governing.checkip("213.152.186.35", function(result)
|
governing.checkip("213.152.186.35", function(result)
|
||||||
@ -100,9 +110,10 @@ Check IP information on data:
|
|||||||
|
|
||||||
## LICENSE
|
## LICENSE
|
||||||
|
|
||||||
Copyright (C) 2023 mckaygerhard <mckaygerhard@gmail.com> CC-BY-SA-NC 4.0
|
* Copyright (C) 2023 mckaygerhard <mckaygerhard@gmail.com> CC-BY-SA-NC 4.0
|
||||||
|
* Copyright (C) 2023 mckayshirou <mckayshirou@gmail.com> CC-BY-SA-NC 4.0
|
||||||
Copyright (C) 2023 mckayshirou <mckayshirou@gmail.com> CC-BY-SA-NC 4.0
|
* Copyright 2016-2017 rnd LGPL v3 for anticheat code of minetest 0.4 engines
|
||||||
|
* Copyright 2021-2023 BuckarooBanzay MIT for beowulf code of mt-mods
|
||||||
|
|
||||||
It applies CC-BY-SA-NC 4.0 unless you ask to request permission
|
It applies CC-BY-SA-NC 4.0 unless you ask to request permission
|
||||||
with special conditions. The ipcheck idea, the mod inclusion,
|
with special conditions. The ipcheck idea, the mod inclusion,
|
||||||
|
608
anticheats.lua
Normal file
608
anticheats.lua
Normal file
@ -0,0 +1,608 @@
|
|||||||
|
|
||||||
|
-------------------------------------------------------------------------
|
||||||
|
-- ANTI PRIVS detects if a custom client without interacting "zips" around the map
|
||||||
|
-- Copyright (c) 2021 mt-mods/BuckarooBanzay MIT
|
||||||
|
-- Copyright 2020-2023 improvements and few fixes, mckaygerhard CC-BY-SA-NC 4.0
|
||||||
|
-------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- per-player data
|
||||||
|
local player_data = {}
|
||||||
|
|
||||||
|
-- obtain player data cheats, improved respect beowulf with missing checks and methods
|
||||||
|
local function get_player_data(name)
|
||||||
|
local player = minetest.get_player_by_name(name)
|
||||||
|
if not player then return nil end
|
||||||
|
if not player_data[name] then
|
||||||
|
player_data[name] = {
|
||||||
|
fliyers = 0, -- number of checks for fly ( added only with governing)
|
||||||
|
strikes = 0, -- number of "strikes" (odd movements)
|
||||||
|
checked = 0, -- number of checks
|
||||||
|
pre_pos = player:get_pos() -- position track (missing in beowulf as bug)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
return player_data[name]
|
||||||
|
-- WARNING pos its always current so checked must be after an interval using callbacks
|
||||||
|
end
|
||||||
|
|
||||||
|
-- clear player data for cheats
|
||||||
|
local function track_player_clear(player)
|
||||||
|
if player then
|
||||||
|
if player:get_player_name() then
|
||||||
|
player_data[player:get_player_name()] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- store player data cheats strikes and checks
|
||||||
|
local function track_player(player)
|
||||||
|
local name = player:get_player_name()
|
||||||
|
local data = get_player_data(name)
|
||||||
|
local pos = player:get_pos()
|
||||||
|
|
||||||
|
if data.pre_pos then
|
||||||
|
-- compare positions
|
||||||
|
local d = vector.distance(pos, data.pre_pos)
|
||||||
|
if d > 200 then
|
||||||
|
data.strikes = data.strikes + 1
|
||||||
|
end
|
||||||
|
data.checked = data.checked + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if data.checked >= 10 then
|
||||||
|
-- check strike-count after 10 movement checks
|
||||||
|
if data.strikes > 8 then
|
||||||
|
-- suspicious movement, log it
|
||||||
|
-- TODO: if this doesn't yield any false-positives, add a kick/ban option
|
||||||
|
local msg = "suspicious movement detected for player: '" .. name .. "'"
|
||||||
|
minetest.log("action", "[governing][beowulf] " .. msg)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- reset counters
|
||||||
|
data.checked = 0
|
||||||
|
data.strikes = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- store current position
|
||||||
|
data.pre_pos = pos
|
||||||
|
end
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------
|
||||||
|
-- ANTI CHEAT for MT4 and old MT5 engines by rnd
|
||||||
|
-- Copyright 2016 rnd LGPL v3
|
||||||
|
-- Copyright 2020-2023 improvements and few fixes, mckaygerhard CC-BY-SA-NC 4.0
|
||||||
|
-------------------------------------------------------------------------
|
||||||
|
local cheat = {};
|
||||||
|
local version = "09/08/2017";
|
||||||
|
|
||||||
|
anticheatsettings = {};
|
||||||
|
anticheatsettings.moderators = {}
|
||||||
|
anticheatsettings.CHEAT_TIMESTEP = tonumber(minetest.settings:get("governing.timestep")) or 15; -- check timestep all players
|
||||||
|
anticheatsettings.CHECK_AGAIN = tonumber(minetest.settings:get("governing.timeagain")) or 15; -- after player found in bad position check again after this to make sure its not lag, this should be larger than expected lag in seconds
|
||||||
|
anticheatsettings.STRING_MODERA = minetest.settings:get("governing.moderators") or "admin,singleplayer";
|
||||||
|
-- moderators list, those players can use cheat debug and will see full cheat message
|
||||||
|
for str in string.gmatch(anticheatsettings.STRING_MODERA, "([^,]+)") do table.insert(anticheatsettings.moderators, str) end
|
||||||
|
|
||||||
|
|
||||||
|
local CHEAT_TIMESTEP = anticheatsettings.CHEAT_TIMESTEP;
|
||||||
|
local CHECK_AGAIN = anticheatsettings.CHECK_AGAIN;
|
||||||
|
cheat.moderators = anticheatsettings.moderators;
|
||||||
|
|
||||||
|
bonemod = minetest.get_modpath("boneworld")
|
||||||
|
|
||||||
|
anticheatdb = {}; -- data about detected cheaters
|
||||||
|
|
||||||
|
cheat.suspect = "";
|
||||||
|
cheat.players = {}; -- temporary cheat detection db
|
||||||
|
cheat.message = "";
|
||||||
|
cheat.debuglist = {}; -- [name]=true -- who gets to see debug msgs
|
||||||
|
|
||||||
|
cheat.scan_timer = 0; -- global scan of players
|
||||||
|
cheat.stat_timer = 0; -- used to collect stats
|
||||||
|
|
||||||
|
cheat.nodelist = {};
|
||||||
|
|
||||||
|
cheat.timestep = CHEAT_TIMESTEP;
|
||||||
|
-- list of forbidden nodes
|
||||||
|
cheat.nodelist = {["default:stone"] = false, ["default:cobble"]= false, ["default:dirt"] = false, ["default:sand"]=false,["default:tree"]= false};
|
||||||
|
|
||||||
|
|
||||||
|
local punish_cheat = function(name)
|
||||||
|
|
||||||
|
local player = minetest.get_player_by_name(name);
|
||||||
|
local ip = tostring(minetest.get_player_ip(name));
|
||||||
|
|
||||||
|
if not player then return end
|
||||||
|
local text=""; local logtext = "";
|
||||||
|
|
||||||
|
if cheat.players[name].cheattype == 1 then
|
||||||
|
text = "#anticheat: ".. name .. " was caught walking inside wall";
|
||||||
|
logtext = os.date("%H:%M.%S").." #anticheat: ".. name .. " was caught walking inside wall at " .. minetest.pos_to_string(cheat.players[name].cheatpos);
|
||||||
|
player:set_hp(0);
|
||||||
|
elseif cheat.players[name].cheattype == 2 then
|
||||||
|
|
||||||
|
local gravity = player:get_physics_override().gravity; if gravity<1 then return end
|
||||||
|
logtext= os.date("%H:%M.%S").." #anticheat: ".. name .. " was caught flying at " .. minetest.pos_to_string(cheat.players[name].cheatpos);
|
||||||
|
if cheat.players[name].cheatpos.y>5 then -- only above height 5 it directly damages flyer
|
||||||
|
text = "#anticheat: ".. name .. " was caught flying";
|
||||||
|
--player:set_hp(0);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if text~="" then
|
||||||
|
minetest.chat_send_all(text);
|
||||||
|
end
|
||||||
|
|
||||||
|
if logtext~="" then
|
||||||
|
minetest.log("action", logtext);
|
||||||
|
cheat.message = logtext;
|
||||||
|
|
||||||
|
anticheatdb[ip] = {name = name, msg = logtext};
|
||||||
|
|
||||||
|
cheat.players[name].count=0; -- reset counter
|
||||||
|
cheat.players[name].cheattype = 0;
|
||||||
|
|
||||||
|
for name,_ in pairs(cheat.moderators) do -- display full message to moderators
|
||||||
|
minetest.chat_send_player(name,logtext);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- CHECKS
|
||||||
|
|
||||||
|
|
||||||
|
-- DETAILED NOCLIP CHECK
|
||||||
|
local check_noclip = function(pos)
|
||||||
|
local nodename = minetest.get_node(pos).name;
|
||||||
|
local clear=true;
|
||||||
|
if nodename ~= "air" then -- check if forbidden material!
|
||||||
|
clear = cheat.nodelist[nodename]; -- test clip violation
|
||||||
|
if clear == nil then clear = true end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not clear then -- more detailed check
|
||||||
|
local anodes = minetest.find_nodes_in_area({x=pos.x-1, y=pos.y-1, z=pos.z-1}, {x=pos.x+1, y=pos.y+1, z=pos.z+1}, {"air"});
|
||||||
|
if #anodes == 0 then return false end
|
||||||
|
clear=true;
|
||||||
|
end
|
||||||
|
return clear;
|
||||||
|
end
|
||||||
|
|
||||||
|
-- DETAILED FLY CHECK
|
||||||
|
local check_fly = function(pos) -- return true if player not flying
|
||||||
|
local fly = (minetest.get_node(pos).name=="air" and minetest.get_node({x=pos.x,y=pos.y-1,z=pos.z}).name=="air"); -- prerequisite for flying is this to be "air", but not sufficient condition
|
||||||
|
if not fly then return true end;
|
||||||
|
|
||||||
|
local anodes = minetest.find_nodes_in_area({x=pos.x-1, y=pos.y-1, z=pos.z-1}, {x=pos.x+1, y=pos.y, z=pos.z+1}, {"air"});
|
||||||
|
if #anodes == 18 then -- player standing on air?
|
||||||
|
return false
|
||||||
|
else
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local round = function (x)
|
||||||
|
if x > 0 then
|
||||||
|
return math.floor(x+0.5)
|
||||||
|
else
|
||||||
|
return -math.floor(-x+0.5)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--main check routine
|
||||||
|
local check_player = function(player)
|
||||||
|
|
||||||
|
local name = player:get_player_name();
|
||||||
|
local privs = minetest.get_player_privs(name).kick;if privs then return end -- dont check moderators
|
||||||
|
|
||||||
|
local pos = player:getpos(); -- feet position
|
||||||
|
pos.x = round(pos.x*10)/10;pos.z = round(pos.z*10)/10; -- less useless clutter
|
||||||
|
pos.y = round(pos.y*10)/10; -- minetest buggy collision - need to do this or it returns wrong materials for feet position: aka magic number 0.498?????228
|
||||||
|
if pos.y<0 then pos.y=pos.y+1 end -- weird, without this it fails to check feet block where y<0, it checks one below feet
|
||||||
|
|
||||||
|
local nodename = minetest.get_node(pos).name;
|
||||||
|
local clear=true;
|
||||||
|
if nodename ~= "air" then -- check if forbidden material!
|
||||||
|
clear = cheat.nodelist[nodename]; -- test clip violation
|
||||||
|
if clear == nil then clear = true end
|
||||||
|
end
|
||||||
|
|
||||||
|
local fly = (nodename=="air" and minetest.get_node({x=pos.x,y=pos.y-1,z=pos.z}).name=="air"); -- prerequisite for flying, but not sufficient condition
|
||||||
|
|
||||||
|
if cheat.players[name].count == 0 then -- player hasnt "cheated" yet, remember last clear position
|
||||||
|
cheat.players[name].clearpos = cheat.players[name].lastpos
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- manage noclip cheats
|
||||||
|
if not clear then -- player caught inside walls
|
||||||
|
local moved = (cheat.players[name].lastpos.x~=pos.x) or (cheat.players[name].lastpos.y~=pos.y) or (cheat.players[name].lastpos.z~=pos.z);
|
||||||
|
if moved then -- if player stands still whole time do nothing
|
||||||
|
if cheat.players[name].count == 0 then cheat.players[name].cheatpos = pos end -- remember first position where player found inside wall
|
||||||
|
|
||||||
|
|
||||||
|
if cheat.players[name].count == 0 then
|
||||||
|
minetest.after(CHECK_AGAIN+math.random(5),
|
||||||
|
function()
|
||||||
|
cheat.players[name].count = 0;
|
||||||
|
if not check_noclip(pos) then
|
||||||
|
punish_cheat(name)-- we got a cheater!
|
||||||
|
else
|
||||||
|
cheat.players[name].count = 0; -- reset
|
||||||
|
cheat.players[name].cheattype = 0;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
if cheat.players[name].count == 0 then -- mark as suspect
|
||||||
|
cheat.players[name].count = 1;
|
||||||
|
cheat.players[name].cheattype = 1;
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- manage flyers
|
||||||
|
if fly then
|
||||||
|
|
||||||
|
local fpos;
|
||||||
|
fly,fpos = minetest.line_of_sight(pos, {x = pos.x, y = pos.y - 4, z = pos.z}, 1); --checking node maximal jump height below feet
|
||||||
|
|
||||||
|
if fly then -- so we are in air, are we flying?
|
||||||
|
|
||||||
|
if player:get_player_control().sneak then -- player sneaks, maybe on border?
|
||||||
|
--search 18 nodes to find non air
|
||||||
|
local anodes = minetest.find_nodes_in_area({x=pos.x-1, y=pos.y-1, z=pos.z-1}, {x=pos.x+1, y=pos.y, z=pos.z+1}, {"air"});
|
||||||
|
if #anodes < 18 then fly = false end
|
||||||
|
end -- if at this point fly = true means player is not standing on border
|
||||||
|
|
||||||
|
if pos.y>=cheat.players[name].lastpos.y and fly then -- we actually didnt go down from last time and not on border
|
||||||
|
|
||||||
|
-- was lastpos in air too?
|
||||||
|
local lastpos = cheat.players[name].lastpos;
|
||||||
|
local anodes = minetest.find_nodes_in_area({x=lastpos.x-1, y=lastpos.y-1, z=lastpos.z-1}, {x=lastpos.x+1, y=lastpos.y, z=lastpos.z+1}, {"air"});
|
||||||
|
if #anodes == 18 then fly = true else fly = false end
|
||||||
|
|
||||||
|
if fly then -- so now in air above previous position, which was in air too?
|
||||||
|
|
||||||
|
if cheat.players[name].count == 0 then cheat.players[name].cheatpos = pos end -- remember first position where player found "cheating"
|
||||||
|
|
||||||
|
if cheat.players[name].count == 0 then
|
||||||
|
minetest.after(CHECK_AGAIN,
|
||||||
|
function()
|
||||||
|
cheat.players[name].count = 0;
|
||||||
|
if not check_fly(pos) then
|
||||||
|
punish_cheat(name)-- we got a cheater!
|
||||||
|
else
|
||||||
|
cheat.players[name].count = 0;
|
||||||
|
cheat.players[name].cheattype = 0;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
if cheat.players[name].count == 0 then -- mark as suspect
|
||||||
|
cheat.players[name].count = 1;
|
||||||
|
cheat.players[name].cheattype = 2;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
cheat.players[name].lastpos = pos
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
minetest.register_globalstep(function(dtime)
|
||||||
|
|
||||||
|
cheat.scan_timer = cheat.scan_timer + dtime
|
||||||
|
|
||||||
|
|
||||||
|
-- GENERAL SCAN OF ALL PLAYERS
|
||||||
|
if cheat.scan_timer>cheat.timestep then
|
||||||
|
|
||||||
|
|
||||||
|
cheat.stat_timer = cheat.stat_timer + cheat.timestep;
|
||||||
|
-- dig xp stats every 2 minutes
|
||||||
|
if bonemod and cheat.stat_timer>120 then
|
||||||
|
cheat.stat_timer = 0;
|
||||||
|
local players = minetest.get_connected_players();
|
||||||
|
for _,player in pairs(players) do
|
||||||
|
local pname = player:get_player_name();
|
||||||
|
if cheat.players[pname].stats.state == 1 then -- only if dig xp loaded to prevent anomalous stats
|
||||||
|
if boneworld.digxp[pname] then
|
||||||
|
local deltadig = cheat.players[pname].stats.digxp;
|
||||||
|
cheat.players[pname].stats.digxp = boneworld.digxp[pname];
|
||||||
|
deltadig = boneworld.digxp[pname]-deltadig;
|
||||||
|
cheat.players[pname].stats.deltadig = deltadig;
|
||||||
|
|
||||||
|
if deltadig>cheat.players[pname].stats.maxdeltadig then
|
||||||
|
cheat.players[pname].stats.maxdeltadig = deltadig;
|
||||||
|
end
|
||||||
|
|
||||||
|
if deltadig>2 then -- unnaturally high deltadig
|
||||||
|
local ip = tostring(minetest.get_player_ip(pname));
|
||||||
|
local logtext = os.date("%H:%M.%S") .. " #anticheat: " .. pname .. " (ip " .. ip .. ") is mining resources too fast, deltadig " .. deltadig;
|
||||||
|
anticheatdb[ip] = {name = pname, msg = logtext};
|
||||||
|
minetest.log("action", logtext);
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
cheat.timestep = CHEAT_TIMESTEP + (2*math.random()-1)*2; -- randomize step so its unpredictable
|
||||||
|
cheat.scan_timer=0;
|
||||||
|
--local t = minetest.get_gametime();
|
||||||
|
local players = minetest.get_connected_players();
|
||||||
|
|
||||||
|
for _,player in pairs(players) do
|
||||||
|
check_player(player);
|
||||||
|
end
|
||||||
|
|
||||||
|
for name,_ in pairs(cheat.debuglist) do -- show suspects in debug
|
||||||
|
for _,player in pairs(players) do
|
||||||
|
local pname = player:get_player_name();
|
||||||
|
if cheat.players[pname].count>0 then
|
||||||
|
minetest.chat_send_player(name, "name " .. pname .. ", cheat pos " .. minetest.pos_to_string(cheat.players[pname].cheatpos) .. " last clear pos " .. minetest.pos_to_string(cheat.players[pname].clearpos) .. " cheat type " .. cheat.players[pname].cheattype .. " cheatcount " .. cheat.players[pname].count );
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- long range dig check
|
||||||
|
|
||||||
|
local check_can_dig = function(pos, digger)
|
||||||
|
|
||||||
|
local p = digger:getpos();
|
||||||
|
if p.y<0 then p.y=p.y+2 else p.y=p.y+1 end -- head position
|
||||||
|
local dist = math.max(math.abs(p.x-pos.x),math.abs(p.y-pos.y),math.abs(p.z-pos.z));
|
||||||
|
|
||||||
|
|
||||||
|
if dist>6 then -- here 5
|
||||||
|
dist = math.floor(dist*100)/100;
|
||||||
|
local pname = digger:get_player_name();
|
||||||
|
local logtext = os.date("%H:%M.%S") .. "#anticheat: long range dig " .. pname ..", distance " .. dist .. ", pos " .. minetest.pos_to_string(pos);
|
||||||
|
for name,_ in pairs(cheat.debuglist) do -- show to all watchers
|
||||||
|
minetest.chat_send_player(name,logtext)
|
||||||
|
|
||||||
|
end
|
||||||
|
local ip = tostring(minetest.get_player_ip(pname));
|
||||||
|
anticheatdb[ip] = {name = pname, msg = logtext};
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local set_check_can_dig = function(name)
|
||||||
|
local tabl = minetest.registered_nodes[name];
|
||||||
|
if not tabl then return end
|
||||||
|
tabl.can_dig = check_can_dig;
|
||||||
|
minetest.override_item(name, {can_dig = check_can_dig})
|
||||||
|
--minetest.register_node(":"..name, tabl);
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
minetest.register_on_joinplayer(function(player) -- init stuff on player join
|
||||||
|
local name = player:get_player_name(); if name == nil then return end
|
||||||
|
local pos = player:getpos();
|
||||||
|
|
||||||
|
if cheat.players[name] == nil then
|
||||||
|
cheat.players[name]={count=0,cheatpos = pos, clearpos = pos, lastpos = pos, cheattype = 0}; -- type 0: none, 1 noclip, 2 fly
|
||||||
|
end
|
||||||
|
|
||||||
|
if cheat.players[name] and cheat.players[name].stats == nil then
|
||||||
|
cheat.players[name].stats = {maxdeltadig=0,deltadig = 0,digxp = 0, state = 0}; -- various statistics about player: max dig xp increase in 2 minutes, current dig xp increase
|
||||||
|
|
||||||
|
minetest.after(5, -- load digxp after boneworld loads it
|
||||||
|
function()
|
||||||
|
if bonemod then
|
||||||
|
if boneworld.xp then
|
||||||
|
cheat.players[name].stats.digxp = boneworld.digxp[name] or 0;
|
||||||
|
cheat.players[name].stats.state = 1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
end
|
||||||
|
--state 0 = stats not loaded yet
|
||||||
|
|
||||||
|
|
||||||
|
local ip = tostring(minetest.get_player_ip(name));
|
||||||
|
local msg = "";
|
||||||
|
|
||||||
|
-- check anticheat db
|
||||||
|
--check ip
|
||||||
|
if anticheatdb[ip] then
|
||||||
|
msg = "#anticheat: welcome back detected cheater, ip = " .. ip .. ", name " .. anticheatdb[ip].name .. ", new name = " .. name;
|
||||||
|
end;
|
||||||
|
--check names
|
||||||
|
for ip,v in pairs(anticheatdb) do
|
||||||
|
if v.name == name then
|
||||||
|
msg = "#anticheat: welcome back detected cheater, ip = " .. ip .. ", name = newname = " .. v.name;
|
||||||
|
break;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if msg~="" then
|
||||||
|
for name,_ in pairs(cheat.moderators) do
|
||||||
|
minetest.chat_send_player(name,msg);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end)
|
||||||
|
|
||||||
|
minetest.register_chatcommand("cchk", {
|
||||||
|
privs = {
|
||||||
|
interact = true
|
||||||
|
},
|
||||||
|
description = "cchk NAME, checks if player is cheating in this moment",
|
||||||
|
func = function(name, param)
|
||||||
|
local privs = minetest.get_player_privs(name).privs;
|
||||||
|
if not cheat.moderators[name] and not privs then return end
|
||||||
|
|
||||||
|
local player = minetest.get_player_by_name(param);
|
||||||
|
if not player then return end
|
||||||
|
check_player(player);
|
||||||
|
|
||||||
|
|
||||||
|
local players = minetest.get_connected_players();
|
||||||
|
for name,_ in pairs(cheat.debuglist) do -- show suspects in debug
|
||||||
|
for _,player in pairs(players) do
|
||||||
|
local pname = player:get_player_name();
|
||||||
|
if cheat.players[pname].count>0 then
|
||||||
|
minetest.chat_send_player(name, "name " .. pname .. ", cheat pos " .. minetest.pos_to_string(cheat.players[pname].cheatpos) .. " last clear pos " .. minetest.pos_to_string(cheat.players[pname].clearpos) .. " cheat type " .. cheat.players[pname].cheattype .. " cheatcount " .. cheat.players[pname].count );
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
minetest.register_chatcommand("crep", { -- see cheat report
|
||||||
|
privs = {
|
||||||
|
interact = true
|
||||||
|
},
|
||||||
|
description = "crep 0/1, 0 = default cheat report, 1 = connected player stats",
|
||||||
|
func = function(name, param)
|
||||||
|
local privs = minetest.get_player_privs(name).privs;
|
||||||
|
if not cheat.moderators[name] and not privs then return end
|
||||||
|
|
||||||
|
if param == "" then
|
||||||
|
minetest.chat_send_player(name,"use: crep type, types: 0(default) cheat report, 1 connected player stats (".. version ..")");
|
||||||
|
end
|
||||||
|
|
||||||
|
param = tonumber(param) or 0;
|
||||||
|
|
||||||
|
if param == 0 then -- show cheat report
|
||||||
|
local text = "";
|
||||||
|
for ip,v in pairs(anticheatdb) do
|
||||||
|
if v and v.name and v.msg then
|
||||||
|
text = text .. "ip " .. ip .. " ".. v.msg .. "\n";
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if text ~= "" then
|
||||||
|
local form = "size [6,7] textarea[0,0;6.5,8.5;creport;CHEAT REPORT;".. text.."]"
|
||||||
|
minetest.show_formspec(name, "anticheatreport", form)
|
||||||
|
end
|
||||||
|
elseif param == 1 then -- show cheat stats
|
||||||
|
local text = "";
|
||||||
|
local players = minetest.get_connected_players();
|
||||||
|
for _,player in pairs(players) do
|
||||||
|
local pname = player:get_player_name();
|
||||||
|
local ip = tostring(minetest.get_player_ip(pname));
|
||||||
|
|
||||||
|
|
||||||
|
text = text .. "\nname " .. pname .. ", digxp " .. math.floor(1000*cheat.players[pname].stats.digxp)/1000 ..
|
||||||
|
", deltadigxp(2min) " .. math.floor(1000*cheat.players[pname].stats.deltadig)/1000 .. ", maxdeltadigxp " .. math.floor(1000*cheat.players[pname].stats.maxdeltadig)/1000; -- .. " ".. string.gsub(dump(cheat.players[pname].stats), "\n", " ");
|
||||||
|
if anticheatdb[ip] then text = text .. " (DETECTED) ip ".. ip .. ", name " .. anticheatdb[ip].name end
|
||||||
|
end
|
||||||
|
if text ~= "" then
|
||||||
|
local form = "size [10,8] textarea[0,0;10.5,9.;creport;CHEAT STATISTICS;".. text.."]"
|
||||||
|
minetest.show_formspec(name, "anticheatreport", form)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- suspects info
|
||||||
|
local players = minetest.get_connected_players();
|
||||||
|
for _,player in pairs(players) do
|
||||||
|
local pname = player:get_player_name();
|
||||||
|
if cheat.players[pname].count>0 then
|
||||||
|
minetest.chat_send_player(name, "name " .. pname .. ", cheat pos " .. minetest.pos_to_string(cheat.players[pname].cheatpos) .. " last clear pos " .. minetest.pos_to_string(cheat.players[pname].lastpos) .. " cheat type " .. cheat.players[pname].cheattype .. " cheatcount " .. cheat.players[pname].count );
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
})
|
||||||
|
|
||||||
|
minetest.register_chatcommand("cdebug", { -- toggle cdebug= display of stats on/off for this player
|
||||||
|
privs = {
|
||||||
|
interact = true
|
||||||
|
},
|
||||||
|
func = function(name, param)
|
||||||
|
local privs = minetest.get_player_privs(name).privs;
|
||||||
|
if not cheat.moderators[name] and not privs then return end
|
||||||
|
|
||||||
|
if cheat.debuglist[name] == nil then cheat.debuglist[name] = true else cheat.debuglist[name] = nil end;
|
||||||
|
|
||||||
|
minetest.chat_send_player(name,"#anticheat: " .. version);
|
||||||
|
if cheat.debuglist[name]==true then
|
||||||
|
minetest.chat_send_player(name,"#anticheat: display of debug messages is ON");
|
||||||
|
else
|
||||||
|
minetest.chat_send_player(name,"#anticheat: display of debug messages is OFF");
|
||||||
|
end
|
||||||
|
end
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------
|
||||||
|
-- GOVERNONR custom action for features mod code improvements
|
||||||
|
-- minetest routines to implement the mod features
|
||||||
|
-- Copyright 2020-2023 mckaygerhard CC-BY-SA-NC 4.0
|
||||||
|
-------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- cleanup after the player leaves
|
||||||
|
minetest.register_on_leaveplayer(function(player)
|
||||||
|
if player then
|
||||||
|
track_player_clear(player)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
|
||||||
|
-- list of nodes to enable damage if noclip or to check if player its diggin too fast
|
||||||
|
local node_list_check = {}
|
||||||
|
|
||||||
|
-- TODO move this to a comma separted list in config, and adde the check below in node check for fly
|
||||||
|
if minetest.get_modpath("default") then
|
||||||
|
table.insert(node_list_check, "default:stone")
|
||||||
|
table.insert(node_list_check, "default:stone_with_iron")
|
||||||
|
table.insert(node_list_check, "default:stone_with_copper")
|
||||||
|
table.insert(node_list_check, "default:stone_with_coal")
|
||||||
|
table.insert(node_list_check, "default:stone_with_gold")
|
||||||
|
table.insert(node_list_check, "default:stone_with_mese")
|
||||||
|
table.insert(node_list_check, "default:stone_with_diamond")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function interval_fn()
|
||||||
|
if not governing.modbeowulf then
|
||||||
|
for _, player in ipairs(minetest.get_connected_players()) do
|
||||||
|
track_player(player)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not governing.modanticheat then
|
||||||
|
for _, nodename in ipairs(node_list_check) do
|
||||||
|
set_check_can_dig(nodename);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
minetest.after(2, interval_fn)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- TODO: beowulf was pretty unneficient on large player servers.. cos only its made each 1 second and again each x inside function
|
||||||
|
if governing.is_50 then
|
||||||
|
minetest.register_on_mods_loaded(interval_fn )
|
||||||
|
else
|
||||||
|
minetest.after(0.1, interval_fn)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- damaged if a player its making noclip, no matter if admin is doing
|
||||||
|
for _, nodename in ipairs(node_list_check) do
|
||||||
|
minetest.override_item(nodename, {
|
||||||
|
damage_per_second = 1
|
||||||
|
})
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
65
antispawn.lua
Normal file
65
antispawn.lua
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
local PLAYERS_MSG = {}
|
||||||
|
local PLAYERS_FREQ = {}
|
||||||
|
local SPAM_SPEED = 5
|
||||||
|
local SPAM_SPEED_MSECS = SPAM_SPEED * 1e6
|
||||||
|
local SPAM_WARN = 5
|
||||||
|
-- In seconds
|
||||||
|
local SPAM_KICK = SPAM_WARN + 5
|
||||||
|
local RESET_TIME = 30
|
||||||
|
-- Convert to microsecs
|
||||||
|
local RESET_TIME_MSECS = RESET_TIME * 1e6
|
||||||
|
local WARNING_COLOR = minetest.get_color_escape_sequence"#FFBB33"
|
||||||
|
minetest.register_on_leaveplayer(function(player)
|
||||||
|
PLAYERS_MSG[player:get_player_name()] = nil
|
||||||
|
PLAYERS_FREQ[player:get_player_name()] = nil
|
||||||
|
end)
|
||||||
|
minetest.register_on_joinplayer(function(player)
|
||||||
|
PLAYERS_MSG[player:get_player_name()] = {}
|
||||||
|
end)
|
||||||
|
minetest.register_on_chat_message(function(name, message)
|
||||||
|
for msg, info in pairs(PLAYERS_MSG[name]) do
|
||||||
|
if minetest.get_us_time() - info[2] >= RESET_TIME_MSECS then PLAYERS_MSG[name][msg] = nil end
|
||||||
|
end
|
||||||
|
if PLAYERS_MSG[name][message] then
|
||||||
|
local amount = PLAYERS_MSG[name][message][1] + 1
|
||||||
|
PLAYERS_MSG[name][message][1] = amount
|
||||||
|
PLAYERS_MSG[name][message][2] = minetest.get_us_time()
|
||||||
|
if amount >= SPAM_KICK then minetest.kick_player(name, "Kicked for spamming.")
|
||||||
|
elseif amount >= SPAM_WARN then
|
||||||
|
minetest.chat_send_player(name, WARNING_COLOR .. "Warning! You've sent the message '" .. message .. "' too often. Wait at least " .. RESET_TIME .. " seconds before sending it again.")
|
||||||
|
end
|
||||||
|
else PLAYERS_MSG[name][message] = { 1, minetest.get_us_time() } end
|
||||||
|
if not PLAYERS_FREQ[name] then
|
||||||
|
PLAYERS_FREQ[name] = {
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
minetest.get_us_time(),
|
||||||
|
0
|
||||||
|
}
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local warns = PLAYERS_FREQ[name][4]
|
||||||
|
local amount = PLAYERS_FREQ[name][2]
|
||||||
|
local speed = PLAYERS_FREQ[name][1]
|
||||||
|
local delay = minetest.get_us_time() - PLAYERS_FREQ[name][3]
|
||||||
|
speed = (speed * amount + delay) / (amount + 1)
|
||||||
|
if amount >= SPAM_WARN then
|
||||||
|
if warns + 1 == SPAM_KICK - SPAM_WARN then minetest.kick_player(name, "Kicked for spamming.")
|
||||||
|
elseif speed <= SPAM_SPEED_MSECS then
|
||||||
|
minetest.chat_send_player(name, WARNING_COLOR .. "Warning! You are sending messages too fast. Wait at least " .. SPAM_SPEED .. " seconds before sending another message.")
|
||||||
|
warns = warns + 1
|
||||||
|
speed = SPAM_SPEED_MSECS
|
||||||
|
amount = SPAM_WARN
|
||||||
|
else
|
||||||
|
speed = 0
|
||||||
|
amount = 0
|
||||||
|
warns = 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
PLAYERS_FREQ[name] = {
|
||||||
|
speed,
|
||||||
|
amount + 1,
|
||||||
|
minetest.get_us_time(),
|
||||||
|
warns
|
||||||
|
}
|
||||||
|
end)
|
@ -1,2 +1,3 @@
|
|||||||
default?
|
default?
|
||||||
mail?
|
mail?
|
||||||
|
boneworld?
|
||||||
|
24
init.lua
24
init.lua
@ -24,6 +24,9 @@ local moddefault = minetest.get_modpath("default") -- path of default mod if ava
|
|||||||
local modkillme = minetest.get_modpath("killme") -- if killme is present as mod if available, otherwise we use own
|
local modkillme = minetest.get_modpath("killme") -- if killme is present as mod if available, otherwise we use own
|
||||||
local modcommand = minetest.get_modpath("game_commands") -- updated killme is present as mod if available, otherwise we use own
|
local modcommand = minetest.get_modpath("game_commands") -- updated killme is present as mod if available, otherwise we use own
|
||||||
local modcreative = minetest.get_modpath("creative") -- if creative is available, otherwise false/nil
|
local modcreative = minetest.get_modpath("creative") -- if creative is available, otherwise false/nil
|
||||||
|
local modantispawn = minetest.get_modpath("antispam") -- if rnd1 anticheat mod is loaded, cos only wolrs with mt4
|
||||||
|
local modanticheat = minetest.get_modpath("anticheat") -- if rnd1 anticheat mod is loaded, cos only wolrs with mt4
|
||||||
|
local modbeowulf = minetest.get_modpath("beowulf") -- if rnd1 anticheat mod is loaded, cos only wolrs with mt4
|
||||||
local checkapikey = minetest.settings:get("governing.checkapikey") or "" -- need for geoip improved ip information, then geoip normal will be used
|
local checkapikey = minetest.settings:get("governing.checkapikey") or "" -- need for geoip improved ip information, then geoip normal will be used
|
||||||
|
|
||||||
local is_46 = minetest.has_feature("add_entity_with_staticdata") -- detect minetest engine 4.0.16 or mayor
|
local is_46 = minetest.has_feature("add_entity_with_staticdata") -- detect minetest engine 4.0.16 or mayor
|
||||||
@ -68,6 +71,9 @@ end
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
--[[ init - namespace of the mod and varible usages ]]
|
||||||
|
|
||||||
governing = {}
|
governing = {}
|
||||||
|
|
||||||
governing.is_46 = is_46 -- detect minetest engine 4.0.16 or mayor
|
governing.is_46 = is_46 -- detect minetest engine 4.0.16 or mayor
|
||||||
@ -87,9 +93,12 @@ governing.modmail = modmail -- path of the mail mod if available for sending mes
|
|||||||
governing.moddefault = moddefault -- path of default mod if available, otherwise false/nil
|
governing.moddefault = moddefault -- path of default mod if available, otherwise false/nil
|
||||||
governing.modkillme = modkillme -- if killme is present as mod if available, otherwise we use own
|
governing.modkillme = modkillme -- if killme is present as mod if available, otherwise we use own
|
||||||
governing.modcommand = modcommand -- if same killme and commands is present as mod if available, otherwise we use own
|
governing.modcommand = modcommand -- if same killme and commands is present as mod if available, otherwise we use own
|
||||||
|
governing.modantispawn = modantispawn -- autodetect if older mod antispan by appguru are present
|
||||||
|
governing.modanticheat = modanticheat -- if rnd1 anticheat mod is present cos only works with mt4
|
||||||
|
governing.modbeowulf = modbeowulf -- if beowulf anticheat mod is present cos only works with mt5
|
||||||
governing.checkapikey = checkapikey -- need for geoip improved ip information, then geoip normal will be used
|
governing.checkapikey = checkapikey -- need for geoip improved ip information, then geoip normal will be used
|
||||||
|
|
||||||
--[[ end namespace of the mod and varible usages ]]
|
--[[ end - namespace of the mod and varible usages ]]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -165,16 +174,25 @@ end
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
--[[ include code files ]]
|
|
||||||
|
--[[ include code files mod features ]]
|
||||||
|
|
||||||
dofile(governing.modpath.."/geoip.lua") -- format the geoip responses managed by "callback(data)"
|
dofile(governing.modpath.."/geoip.lua") -- format the geoip responses managed by "callback(data)"
|
||||||
|
|
||||||
|
dofile(governing.modpath.."/anticheats.lua") -- load anticheat mod that combined rnd + beowulf + fairplay
|
||||||
|
|
||||||
|
dofile(governing.modpath.."/antispawn.lua") -- load antispawn chat features only if mod antispan is not loaded
|
||||||
|
|
||||||
dofile(governing.modpath.."/commands.lua") -- must be at end. so relies on others functionalities
|
dofile(governing.modpath.."/commands.lua") -- must be at end. so relies on others functionalities
|
||||||
|
|
||||||
|
--[[ end of include code files mod features ]]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--[[ log files and finished of mod loading ]]
|
--[[ log files and finished of mod loading ]]
|
||||||
|
|
||||||
if governing.checkapikey == "" then minetest.log("warning", "[goberning] governing.checkapikey not set or empty") end
|
if governing.checkapikey == "" then minetest.log("warning", "[goberning] governing.checkapikey not set or empty, only basic geoip will work") end
|
||||||
|
|
||||||
print("[MOD] governing mod loaded" )
|
print("[MOD] governing mod loaded" )
|
||||||
|
|
||||||
|
2
mod.conf
2
mod.conf
@ -1,2 +1,2 @@
|
|||||||
name = governing
|
name = governing
|
||||||
optional_depends = default, mail
|
optional_depends = default, mail, boneworld
|
||||||
|
@ -1 +1,4 @@
|
|||||||
governing.checkapikey (The string need for use the https://vpnapi.io/ ip informacion with geolocation) string
|
governing.checkapikey (The string need for use the https://vpnapi.io/ ip informacion with geolocation) string
|
||||||
|
governing.timestep (How many time will run checks in seconds) int 15 10 300
|
||||||
|
governing.timeagain (How many seconds checks again to compare if it is cheating the suspected player) int 120 10 300
|
||||||
|
governing.moderators (Comma separated list of name players that will be not checked, without spaces) string
|
||||||
|
Loading…
x
Reference in New Issue
Block a user