From ab2292dfd5903378d26afe29c144eb66ce7f1f94 Mon Sep 17 00:00:00 2001 From: kevinz000 <2003111+kevinz000@users.noreply.github.com> Date: Tue, 7 Jan 2020 04:48:39 -0800 Subject: [PATCH] Fail2Topic --- code/__HELPERS/_logging.dm | 5 +- .../configuration/entries/fail2topic.dm | 15 +++ code/controllers/master.dm | 4 +- code/controllers/subsystem.dm | 4 +- code/controllers/subsystem/fail2topic.dm | 107 ++++++++++++++++++ code/game/world.dm | 10 ++ code/modules/tgs/v3210/commands.dm | 32 +++--- config/config.txt | 12 ++ tgstation.dme | 4 +- 9 files changed, 171 insertions(+), 22 deletions(-) create mode 100644 code/controllers/configuration/entries/fail2topic.dm create mode 100644 code/controllers/subsystem/fail2topic.dm diff --git a/code/__HELPERS/_logging.dm b/code/__HELPERS/_logging.dm index 3ee77d3edc..241e9b906e 100644 --- a/code/__HELPERS/_logging.dm +++ b/code/__HELPERS/_logging.dm @@ -78,7 +78,6 @@ if (CONFIG_GET(flag/log_manifest)) WRITE_LOG(GLOB.world_manifest_log, "[ckey] \\ [body.real_name] \\ [mind.assigned_role] \\ [mind.special_role ? mind.special_role : "NONE"] \\ [latejoin ? "LATEJOIN":"ROUNDSTART"]") - /proc/log_say(text) if (CONFIG_GET(flag/log_say)) WRITE_LOG(GLOB.world_game_log, "SAY: [text]") @@ -121,7 +120,6 @@ if (CONFIG_GET(flag/log_vote)) WRITE_LOG(GLOB.world_game_log, "VOTE: [text]") - /proc/log_topic(text) WRITE_LOG(GLOB.world_game_log, "TOPIC: [text]") @@ -141,6 +139,9 @@ if (CONFIG_GET(flag/log_job_debug)) WRITE_LOG(GLOB.world_job_debug_log, "JOB: [text]") +/proc/log_ss(subsystem, text) + WRITE_LOG(GLOB.subsystem_log, "[subsystem]: [text]") + /* Log to both DD and the logfile. */ /proc/log_world(text) #ifdef USE_CUSTOM_ERROR_HANDLER diff --git a/code/controllers/configuration/entries/fail2topic.dm b/code/controllers/configuration/entries/fail2topic.dm new file mode 100644 index 0000000000..665a55dd0f --- /dev/null +++ b/code/controllers/configuration/entries/fail2topic.dm @@ -0,0 +1,15 @@ +/datum/config_entry/number/fail2topic_rate_limit + config_entry_value = 10 //deciseconds + +/datum/config_entry/number/fail2topic_max_fails + config_entry_value = 5 + +/datum/config_entry/string/fail2topic_rule_name + config_entry_value = "_dd_fail2topic" + protection = CONFIG_ENTRY_LOCKED //affects physical server configuration, no touchies!! + +/datum/config_entry/flag/fail2topic_enabled + config_entry_value = TRUE + +/datum/config_entry/number/topic_max_size + config_entry_value = 8192 diff --git a/code/controllers/master.dm b/code/controllers/master.dm index 125da84a30..7244212630 100644 --- a/code/controllers/master.dm +++ b/code/controllers/master.dm @@ -54,7 +54,7 @@ GLOBAL_REAL(Master, /datum/controller/master) = new var/static/restart_clear = 0 var/static/restart_timeout = 0 var/static/restart_count = 0 - + var/static/random_seed //current tick limit, assigned before running a subsystem. @@ -69,7 +69,7 @@ GLOBAL_REAL(Master, /datum/controller/master) = new if(!random_seed) random_seed = (TEST_RUN_PARAMETER in world.params) ? 29051994 : rand(1, 1e9) rand_seed(random_seed) - + var/list/_subsystems = list() subsystems = _subsystems if (Master != src) diff --git a/code/controllers/subsystem.dm b/code/controllers/subsystem.dm index 4fe0812c56..3be4f36270 100644 --- a/code/controllers/subsystem.dm +++ b/code/controllers/subsystem.dm @@ -155,6 +155,8 @@ if(SS_SLEEPING) state = SS_PAUSING +/datum/controller/subsystem/proc/subsystem_log(msg) + return log_subsystem(name, msg) //used to initialize the subsystem AFTER the map has loaded /datum/controller/subsystem/Initialize(start_timeofday) @@ -162,7 +164,7 @@ var/time = (REALTIMEOFDAY - start_timeofday) / 10 var/msg = "Initialized [name] subsystem within [time] second[time == 1 ? "" : "s"]!" to_chat(world, "[msg]") - log_world(msg) + log_subsystem("INIT", msg) return time //hook for printing stats to the "MC" statuspanel for admins to see performance and related stats etc. diff --git a/code/controllers/subsystem/fail2topic.dm b/code/controllers/subsystem/fail2topic.dm new file mode 100644 index 0000000000..01b6d225b9 --- /dev/null +++ b/code/controllers/subsystem/fail2topic.dm @@ -0,0 +1,107 @@ +SUBSYSTEM_DEF(fail2topic) + name = "Fail2Topic" + init_order = SS_INIT_MISC_FIRST + flags = SS_FIRE_IN_LOBBY | SS_BACKGROUND + + var/list/rate_limiting = list() + var/list/fail_counts = list() + var/list/active_bans = list() + + var/rate_limit + var/max_fails + var/rule_name + var/enabled = FALSE + +/datum/controller/subsystem/fail2topic/Initialize(timeofday) + rate_limit = CONFIG_GET(number/fail2topic_rate_limit) + max_fails = CONFIG_GET(number/fail2topic_max_fails) + rule_name = CONFIG_GET(string/fail2topic_rule_name) + enabled = CONFIG_GET(flag/fail2topic_enabled) + + DropFirewallRule() // Clear the old bans if any still remain + + if (world.system_type == UNIX && enabled) + enabled = FALSE + subsystem_log("DISABLED - UNIX systems are not supported.") + if(!enabled) + flags |= SS_NO_FIRE + can_fire = FALSE + + return ..() + +/datum/controller/subsystem/fail2topic/fire() + while (rate_limiting.len) + var/ip = rate_limiting[1] + var/last_attempt = rate_limiting[ip] + + if (world.time - last_attempt > rate_limit) + rate_limiting -= ip + fail_counts -= ip + + if (MC_TICK_CHECK) + return + +/datum/controller/subsystem/fail2topic/Shutdown() + DropFirewallRule() + +/datum/controller/subsystem/fail2topic/proc/IsRateLimited(ip) + var/last_attempt = rate_limiting[ip] + + if (config?.api_rate_limit_whitelist[ip]) + return FALSE + + if (active_bans[ip]) + return TRUE + + rate_limiting[ip] = world.time + + if (isnull(last_attempt)) + return FALSE + + if (world.time - last_attempt > rate_limit) + fail_counts -= ip + return FALSE + else + var/failures = fail_counts[ip] + + if (isnull(failures)) + fail_counts[ip] = 1 + return FALSE + else if (failures > max_fails) + BanFromFirewall(ip) + return TRUE + else + fail_counts[ip] = failures + 1 + return TRUE + +/datum/controller/subsystem/fail2topic/proc/BanFromFirewall(ip) + if (!enabled) + return + + active_bans[ip] = world.time + fail_counts -= ip + rate_limiting -= ip + + . = shell("netsh advfirewall firewall add rule name=\"[rule_name]\" dir=in interface=any action=block remoteip=[ip]") + + if (.) + subsystem_log("Failed to ban [ip]. Exit code: [.].") + else if (isnull(.)) + subsystem_log("Failed to invoke shell to ban [ip].") + else + subsystem_log("Banned [ip].") + +/datum/controller/subsystem/fail2topic/proc/DropFirewallRule() + if (!enabled) + return + + active_bans = list() + + . = shell("netsh advfirewall firewall delete rule name=\"[rule_name]\"") + + if (.) + subsystem_log("Failed to drop firewall rule. Exit code: [.].") + else if (isnull(.)) + subsystem_log("Failed to invoke shell for firewall rule drop.") + else + subsystem_log("Firewall rule dropped.") diff --git a/code/game/world.dm b/code/game/world.dm index 4043f15f6f..99701c34dd 100644 --- a/code/game/world.dm +++ b/code/game/world.dm @@ -112,6 +112,7 @@ GLOBAL_VAR(restart_counter) GLOB.world_runtime_log = "[GLOB.log_directory]/runtime.log" GLOB.query_debug_log = "[GLOB.log_directory]/query_debug.log" GLOB.world_job_debug_log = "[GLOB.log_directory]/job_debug.log" + GLOB.subsystem_log = "[GLOB.log_directory]/subsystem.log" #ifdef UNIT_TESTS GLOB.test_log = file("[GLOB.log_directory]/tests.log") @@ -126,6 +127,7 @@ GLOBAL_VAR(restart_counter) start_log(GLOB.world_qdel_log) start_log(GLOB.world_runtime_log) start_log(GLOB.world_job_debug_log) + start_log(GLOB.subsystem_log) GLOB.changelog_hash = md5('html/changelog.html') //for telling if the changelog has changed recently if(fexists(GLOB.config_error_log)) @@ -143,6 +145,14 @@ GLOBAL_VAR(restart_counter) /world/Topic(T, addr, master, key) TGS_TOPIC //redirect to server tools if necessary + if(!SSfail2topic) + return "Server not initialized." + else if(!SSfail2topic.IsRateLimited(addr)) + return "Rate limited." + + if(length(T) > CONFIG_GET(number/topic_max_size)) + return "Payload too large!" + var/static/list/topic_handlers = TopicHandlers() var/list/input = params2list(T) diff --git a/code/modules/tgs/v3210/commands.dm b/code/modules/tgs/v3210/commands.dm index 71d7e32366..e674fd4e78 100644 --- a/code/modules/tgs/v3210/commands.dm +++ b/code/modules/tgs/v3210/commands.dm @@ -19,7 +19,7 @@ TGS_ERROR_LOG("Custom command [command_name] can't be used as it is empty or contains illegal characters!") warned_command_names[command_name] = TRUE continue - + if(command_name_types[command_name]) if(warnings_only) TGS_ERROR_LOG("Custom commands [command_name_types[command_name]] and [stc] have the same name, only [command_name_types[command_name]] will be available!") @@ -55,24 +55,24 @@ The MIT License Copyright (c) 2017 Jordan Brown -Permission is hereby granted, free of charge, -to any person obtaining a copy of this software and -associated documentation files (the "Software"), to -deal in the Software without restriction, including -without limitation the rights to use, copy, modify, -merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom -the Software is furnished to do so, +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR -ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ diff --git a/config/config.txt b/config/config.txt index e71c2587b7..fafb3e5791 100644 --- a/config/config.txt +++ b/config/config.txt @@ -473,3 +473,15 @@ DISABLE_HIGH_POP_MC_MODE_AMOUNT 60 ## For reference, Goonstation uses a resolution of 21x15 for it's widescreen mode. ## Do note that changing this value will affect the title screen. The title screen will have to be updated manually if this is changed. DEFAULT_VIEW 21x15 + +### FAIL2TOPIC: +### Automated IP bans for world/Topic() spammers +## Enabled +FAIL2TOPIC_ENABLED +## Minimum wait time in deciseconds between valid requests +FAIL2TOPIC_RATE_LIMIT 10 +## Number of requests after breaching rate limit that triggers a ban +FAIL2TOPIC_MAX_FAILS 5 +## Firewall rule name used on physical server +FAIL2TOPIC_RULE_NAME _dd_fail2topic + diff --git a/tgstation.dme b/tgstation.dme index 1a79c32dcb..38645a3030 100755 --- a/tgstation.dme +++ b/tgstation.dme @@ -229,6 +229,7 @@ #include "code\controllers\configuration\entries\dbconfig.dm" #include "code\controllers\configuration\entries\donator.dm" #include "code\controllers\configuration\entries\dynamic.dm" +#include "code\controllers\configuration\entries\fail2topic.dm" #include "code\controllers\configuration\entries\game_options.dm" #include "code\controllers\configuration\entries\general.dm" #include "code\controllers\subsystem\acid.dm" @@ -245,6 +246,7 @@ #include "code\controllers\subsystem\dcs.dm" #include "code\controllers\subsystem\disease.dm" #include "code\controllers\subsystem\events.dm" +#include "code\controllers\subsystem\fail2topic.dm" #include "code\controllers\subsystem\fire_burning.dm" #include "code\controllers\subsystem\garbage.dm" #include "code\controllers\subsystem\icon_smooth.dm" @@ -470,8 +472,8 @@ #include "code\datums\elements\_element.dm" #include "code\datums\elements\cleaning.dm" #include "code\datums\elements\earhealing.dm" -#include "code\datums\elements\wuv.dm" #include "code\datums\elements\ghost_role_eligibility.dm" +#include "code\datums\elements\wuv.dm" #include "code\datums\helper_datums\events.dm" #include "code\datums\helper_datums\getrev.dm" #include "code\datums\helper_datums\icon_snapshot.dm"