diff --git a/SQL/paradise_schema.sql b/SQL/paradise_schema.sql index 0379517190f..750add2fd86 100644 --- a/SQL/paradise_schema.sql +++ b/SQL/paradise_schema.sql @@ -535,3 +535,17 @@ CREATE TABLE IF NOT EXISTS `discord` ( PRIMARY KEY (`ckey`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `ipintel` +-- +DROP TABLE IF EXISTS `ipintel`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `ipintel` ( +`ip` INT UNSIGNED NOT NULL , +`date` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL , +`intel` REAL NOT NULL DEFAULT '0', +PRIMARY KEY ( `ip` ) +) ENGINE = INNODB; +/*!40101 SET character_set_client = @saved_cs_client */; diff --git a/SQL/paradise_schema_prefixed.sql b/SQL/paradise_schema_prefixed.sql index e9a622cf037..1a663563cab 100644 --- a/SQL/paradise_schema_prefixed.sql +++ b/SQL/paradise_schema_prefixed.sql @@ -534,3 +534,17 @@ CREATE TABLE IF NOT EXISTS `SS13_discord` ( PRIMARY KEY (`ckey`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `SS13_ipintel` +-- +DROP TABLE IF EXISTS `SS13_ipintel`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `SS13_ipintel` ( +`ip` INT UNSIGNED NOT NULL , +`date` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL , +`intel` REAL NOT NULL DEFAULT '0', +PRIMARY KEY ( `ip` ) +) ENGINE = INNODB; +/*!40101 SET character_set_client = @saved_cs_client */; \ No newline at end of file diff --git a/SQL/updates/5-6.sql b/SQL/updates/5-6.sql new file mode 100644 index 00000000000..73362bacd99 --- /dev/null +++ b/SQL/updates/5-6.sql @@ -0,0 +1,6 @@ +CREATE TABLE `ipintel` ( +`ip` INT UNSIGNED NOT NULL , +`date` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL , +`intel` REAL NOT NULL DEFAULT '0', +PRIMARY KEY ( `ip` ) +) ENGINE = INNODB; \ No newline at end of file diff --git a/code/controllers/configuration.dm b/code/controllers/configuration.dm index 01cf57aad9e..53affcff45c 100644 --- a/code/controllers/configuration.dm +++ b/code/controllers/configuration.dm @@ -123,6 +123,14 @@ var/slime_delay = 0 var/animal_delay = 0 + //IP Intel vars + var/ipintel_email + var/ipintel_rating_bad = 1 + var/ipintel_save_good = 12 + var/ipintel_save_bad = 1 + var/ipintel_domain = "check.getipintel.net" + var/ipintel_maxplaytime = 0 + var/admin_legacy_system = 0 //Defines whether the server uses the legacy admin system with admins.txt or the SQL system. Config option in config.txt var/ban_legacy_system = 0 //Defines whether the server uses the legacy banning system with the files in /data or the SQL system. Config option in config.txt var/use_age_restriction_for_jobs = 0 //Do jobs use account age restrictions? --requires database @@ -213,7 +221,7 @@ // Automatic localhost admin disable var/disable_localhost_admin = 0 - + //Start now warning var/start_now_confirmation = 0 @@ -293,6 +301,20 @@ if("shadowling_max_age") config.shadowling_max_age = text2num(value) + if("ipintel_email") + if (value != "ch@nge.me") + config.ipintel_email = value + if("ipintel_rating_bad") + config.ipintel_rating_bad = text2num(value) + if("ipintel_domain") + config.ipintel_domain = value + if("ipintel_save_good") + config.ipintel_save_good = text2num(value) + if("ipintel_save_bad") + config.ipintel_save_bad = text2num(value) + if("ipintel_maxplaytime") + config.ipintel_maxplaytime = text2num(value) + if("log_ooc") config.log_ooc = 1 @@ -358,10 +380,10 @@ if("no_dead_vote") config.vote_no_dead = 1 - + if("vote_autotransfer_initial") config.vote_autotransfer_initial = text2num(value) - + if("vote_autotransfer_interval") config.vote_autotransfer_interval = text2num(value) @@ -645,7 +667,7 @@ if("disable_karma") config.disable_karma = 1 - + if("start_now_confirmation") config.start_now_confirmation = 1 diff --git a/code/controllers/subsystem/ipintel.dm b/code/controllers/subsystem/ipintel.dm new file mode 100644 index 00000000000..6e31d903622 --- /dev/null +++ b/code/controllers/subsystem/ipintel.dm @@ -0,0 +1,14 @@ +SUBSYSTEM_DEF(ipintel) + name = "XKeyScore" + wait = 1 + flags = SS_NO_FIRE + var/enabled = 0 //disable at round start to avoid checking reconnects + var/throttle = 0 + var/errors = 0 + + var/list/cache = list() + +/datum/controller/subsystem/ipintel/Initialize(timeofday, zlevel) + enabled = 1 + . = ..() + diff --git a/code/modules/admin/ipintel.dm b/code/modules/admin/ipintel.dm new file mode 100644 index 00000000000..5ef982b6079 --- /dev/null +++ b/code/modules/admin/ipintel.dm @@ -0,0 +1,138 @@ +/datum/ipintel + var/ip + var/intel = 0 + var/cache = FALSE + var/cacheminutesago = 0 + var/cachedate = "" + var/cacherealtime = 0 + +/datum/ipintel/New() + cachedate = SQLtime() + cacherealtime = world.realtime + +/datum/ipintel/proc/is_valid() + . = FALSE + if (intel < 0) + return + if (intel <= config.ipintel_rating_bad) + if (world.realtime < cacherealtime + (config.ipintel_save_good * 60 * 60 * 10)) + return TRUE + else + if (world.realtime < cacherealtime + (config.ipintel_save_bad * 60 * 60 * 10)) + return TRUE + +/proc/get_ip_intel(ip, bypasscache = FALSE, updatecache = TRUE) + var/datum/ipintel/res = new() + res.ip = ip + . = res + if (!ip || !config.ipintel_email || !SSipintel.enabled) + return + if (!bypasscache) + var/datum/ipintel/cachedintel = SSipintel.cache[ip] + if (cachedintel && cachedintel.is_valid()) + cachedintel.cache = TRUE + return cachedintel + + if(dbcon.IsConnected()) + var/rating_bad = config.ipintel_rating_bad + var/DBQuery/query_get_ip_intel = dbcon.NewQuery({" + SELECT date, intel, TIMESTAMPDIFF(MINUTE,date,NOW()) + FROM [format_table_name("ipintel")] + WHERE + ip = INET_ATON('[ip]') + AND (( + intel < [rating_bad] + AND + date + INTERVAL [config.ipintel_save_good] HOUR > NOW() + ) OR ( + intel >= [rating_bad] + AND + date + INTERVAL [config.ipintel_save_bad] HOUR > NOW() + )) + "}) + if(!query_get_ip_intel.Execute()) + qdel(query_get_ip_intel) + return + if (query_get_ip_intel.NextRow()) + res.cache = TRUE + res.cachedate = query_get_ip_intel.item[1] + res.intel = text2num(query_get_ip_intel.item[2]) + res.cacheminutesago = text2num(query_get_ip_intel.item[3]) + res.cacherealtime = world.realtime - (text2num(query_get_ip_intel.item[3])*10*60) + SSipintel.cache[ip] = res + qdel(query_get_ip_intel) + return + qdel(query_get_ip_intel) + res.intel = ip_intel_query(ip) + if (updatecache && res.intel >= 0) + SSipintel.cache[ip] = res + if(dbcon.IsConnected()) + var/DBQuery/query_add_ip_intel = dbcon.NewQuery("INSERT INTO [format_table_name("ipintel")] (ip, intel) VALUES (INET_ATON('[ip]'), [res.intel]) ON DUPLICATE KEY UPDATE intel = VALUES(intel), date = NOW()") + query_add_ip_intel.Execute() + qdel(query_add_ip_intel) + + +/proc/ip_intel_query(ip, var/retryed=0) + . = -1 //default + if (!ip) + return + if (SSipintel.throttle > world.timeofday) + return + if (!SSipintel.enabled) + return + + var/list/http[] = world.Export("http://[config.ipintel_domain]/check.php?ip=[ip]&contact=[config.ipintel_email]&format=json&flags=f") + + if (http) + var/status = text2num(http["STATUS"]) + + if (status == 200) + var/response = json_decode(file2text(http["CONTENT"])) + if (response) + if (response["status"] == "success") + var/intelnum = text2num(response["result"]) + if (isnum(intelnum)) + return text2num(response["result"]) + else + ipintel_handle_error("Bad intel from server: [response["result"]].", ip, retryed) + if (!retryed) + sleep(25) + return .(ip, 1) + else + ipintel_handle_error("Bad response from server: [response["status"]].", ip, retryed) + if (!retryed) + sleep(25) + return .(ip, 1) + + else if (status == 429) + ipintel_handle_error("Error #429: We have exceeded the rate limit.", ip, 1) + return + else + ipintel_handle_error("Unknown status code: [status].", ip, retryed) + if (!retryed) + sleep(25) + return .(ip, 1) + else + ipintel_handle_error("Unable to connect to API.", ip, retryed) + if (!retryed) + sleep(25) + return .(ip, 1) + + +/proc/ipintel_handle_error(error, ip, retryed) + if (retryed) + SSipintel.errors++ + error += " Could not check [ip]. Disabling IPINTEL for [SSipintel.errors] minute[( SSipintel.errors == 1 ? "" : "s" )]" + SSipintel.throttle = world.timeofday + (10 * 120 * SSipintel.errors) + else + error += " Attempting retry on [ip]." + log_ipintel(error) + +/proc/log_ipintel(text) + log_game("IPINTEL: [text]") + log_debug("IPINTEL: [text]") + + + + + diff --git a/code/modules/client/client defines.dm b/code/modules/client/client defines.dm index 00900fe5fd1..b9a6df2be00 100644 --- a/code/modules/client/client defines.dm +++ b/code/modules/client/client defines.dm @@ -71,6 +71,8 @@ control_freak = CONTROL_FREAK_ALL | CONTROL_FREAK_SKIN | CONTROL_FREAK_MACROS + var/ip_intel = "Disabled" + var/datum/click_intercept/click_intercept = null //datum that controls the displaying and hiding of tooltips diff --git a/code/modules/client/client procs.dm b/code/modules/client/client procs.dm index a6216c87b1a..8c4248593b6 100644 --- a/code/modules/client/client procs.dm +++ b/code/modules/client/client procs.dm @@ -374,6 +374,7 @@ to_chat(src, message) clientmessages.Remove(ckey) + check_ip_intel() send_resources() @@ -528,6 +529,19 @@ var/DBQuery/query_accesslog = dbcon.NewQuery("INSERT INTO `[format_table_name("connection_log")]`(`id`,`datetime`,`serverip`,`ckey`,`ip`,`computerid`) VALUES(null,Now(),'[serverip]','[ckey]','[sql_ip]','[sql_computerid]');") query_accesslog.Execute() +/client/proc/check_ip_intel() + set waitfor = 0 //we sleep when getting the intel, no need to hold up the client connection while we sleep + if (config.ipintel_email) + if(config.ipintel_maxplaytime && config.use_exp_tracking) + var/living_hours = get_exp_type_num(EXP_TYPE_LIVING) / 60 + if(living_hours >= config.ipintel_maxplaytime) + message_admins("Proxy Detection: [key_name_admin(src)] skips check due to [living_hours] playtime.") + return + + var/datum/ipintel/res = get_ip_intel(address) + if (res.intel >= config.ipintel_rating_bad) + message_admins("Proxy Detection: [key_name_admin(src)] IP intel rated [res.intel*100]% likely to be a Proxy/VPN.") + ip_intel = res.intel #undef TOPIC_SPAM_DELAY #undef UPLOAD_LIMIT diff --git a/config/example/config.txt b/config/example/config.txt index d93809c32cd..4a011939567 100644 --- a/config/example/config.txt +++ b/config/example/config.txt @@ -176,6 +176,21 @@ GUEST_JOBBAN ## Uncomment this to stop people connecting to your server without a registered ckey. (i.e. guest-* are all blocked from connecting) GUEST_BAN +### IPINTEL: +### This allows you to detect likely proxies by checking ips against getipintel.net +## Rating to warn at: (0.90 is good, 1 is 100% likely to be a spammer/proxy, 0.8 is 80%, etc) anything equal to or higher then this number triggers an admin warning +#IPINTEL_RATING_BAD 0.90 +## Contact email, (required to use the service, leaving blank or default disables IPINTEL) +#IPINTEL_EMAIL ch@nge.me +## How long to save good matches (ipintel rate limits to 15 per minute and 500 per day. so this shouldn't be too low, getipintel.net suggests 6 hours, time is in hours) (Your ip will get banned if you go over 500 a day too many times) +#IPINTEL_SAVE_GOOD 72 +## How long to save bad matches (these numbers can change as ips change hands, best not to save these for too long in case somebody gets a new ip used by a spammer/proxy before.) +#IPINTEL_SAVE_BAD 3 +## Domain name to query (leave commented out for the default, only needed if you pay getipintel.net for more querys) +#IPINTEL_DOMAIN check.getipintel.net +## Ignore players with this many hours of playtime. Requires USE_EXP_TRACKING +#IPINTEL_MAXPLAYTIME 10 + ## Comment to disable checking for the cid randomizer dll. (disabled if database isn't enabled or connected) CHECK_RANDOMIZER diff --git a/paradise.dme b/paradise.dme index e478048e4d8..013c04fa842 100644 --- a/paradise.dme +++ b/paradise.dme @@ -212,6 +212,7 @@ #include "code\controllers\subsystem\fires.dm" #include "code\controllers\subsystem\garbage.dm" #include "code\controllers\subsystem\icon_smooth.dm" +#include "code\controllers\subsystem\ipintel.dm" #include "code\controllers\subsystem\jobs.dm" #include "code\controllers\subsystem\machinery.dm" #include "code\controllers\subsystem\mapping.dm" @@ -1128,6 +1129,7 @@ #include "code\modules\admin\create_poll.dm" #include "code\modules\admin\create_turf.dm" #include "code\modules\admin\holder2.dm" +#include "code\modules\admin\ipintel.dm" #include "code\modules\admin\IsBanned.dm" #include "code\modules\admin\machine_upgrade.dm" #include "code\modules\admin\NewBan.dm"