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"