--[[ | This file was obtained through the combined efforts | of Madbluntz & Plymouth Antiquarian Society. | | Credits: lifestorm, Gregory Wayne Rossel JR., | Maloy, DrPepper10 @ RIP, Atle! | | Visit for more: https://plymouth.thetwilightzone.ru/ --]] local pcall = pcall local util = util local ix = ix local string = string local net = net local ipairs = ipairs local IsValid = IsValid local os = os local L = L local math = math local table = table local pairs = pairs local print = print local CHTTP = CHTTP local player = player local CAMI = CAMI if (!CHTTP) then pcall(require, "chttp") end local PLUGIN = PLUGIN PLUGIN.TYPE_UNKNOWN = 1 PLUGIN.TYPE_KNOWN = 2 local COOKIE_KEY = "qvFT4QSSST8K4tZF" local fileChecks = { {"hl2_misc_001.vpk", "hl2"}, {"ep2_pak_008.vpk", "ep2"}, {"cstrike_pak_003.vpk", "css", "cstrike"}, {"bin/client_panorama.dll", "csgo"}, {"detail.vbsp", "gmod", "garrysmod"} } util.AddNetworkString("RecieveDupe") ix.util.Include("sv_antialt_db.lua") ix.lang.AddTable("english", { altSignupProxy = "Vous essayez de rejoindre Willard Networks en tant que nouvel utilisateur tout en utilisant un proxy ou un VPN. Pour lutter contre les autres comptes, nous ne l'autorisons pas.\n\nVeuillez désactiver votre proxy/VPN et rejoindre le serveur. Après avoir rejoint le serveur avec succès une fois, vous pouvez réactiver votre proxy/VPN pour de futures visites.\n\nSi vous n'utilisez pas de proxy ou de VPN, veuillez contacter un membre de l'équipe du STAFF sur nos forums ou discord" }) ix.log.AddType("altNewClient", function(client) return string.format("[AltChecker] %s rejoint pour la première fois.", client:SteamName()) end) ix.log.AddType("altKnownNewCookie", function(client) return string.format("[AltChecker] %s rejoint avec une installation inconnue.", client:SteamName()) end) ix.log.AddType("altKnownCookieMatched", function(client, matched, total) return string.format("[AltChecker] %s rejoint sans cookie, mais l'installation correspond au cookie existant. %d/%d", client:SteamName(), matched, total) end) hook.Add("PlayerInitialSpawn", "bastionAntiAlt", function(client) if (client:IsBot()) then return end client.ixAltData = { checks = 4 + #fileChecks, altLogging = 0, error = false, received = false, checkComplete = false, newAltFound = false, mode = 0, localCookie = "", cookies = false, ip = false, timestamps = false, otherCookies = {}, otherIPs = {}, otherTimestamps = {}, otherTimestampsNZ = {}, otherTimeMatches = {}, discordAlert = { cookies = {}, timestamps = {}, ips = {}, altIDs = {} } } -- Lookup user data PLUGIN:AltLoadUserData(client) if (PLUGIN.API_KEY) then -- Lookup user IP PLUGIN:AltLookupIP(client) else client.ixAltData.checkes = client.ixAltData.checks - 1 end -- Check for IP matches PLUGIN:AltLookupIPMatches(client) -- Request cookie and install timestamps PLUGIN:RequestClientData(client) end) function PLUGIN:RequestClientData(client) net.Start("RecieveDupe") net.WriteUInt(1, 3) net.WriteString(COOKIE_KEY) for _, v in ipairs(fileChecks) do net.WriteString(v[1]) net.WriteString(v[3] or v[2]) end net.Send(client) end net.Receive("RecieveDupe", function(len, client) if (!IsValid(client) or !client.ixAltData or client.ixAltData.received) then return end local data = client.ixAltData data.received = true -- set cookie data.localCookie = net.ReadString() -- set file timestamps data.timestamps = {} for i = 1, #fileChecks do data.timestamps[i] = net.ReadUInt(32) end -- Check for cookie matches if (data.localCookie != "") then PLUGIN:AltLookupCookieMatches(client, data) else PLUGIN:AltPreFinalChecking(client) end -- Check for install timestamp matches data.otherTimestamps = {} data.otherTimestampsNZ = {} for i = 1, #fileChecks do PLUGIN:AltLookupTimestampMatches(client, data, fileChecks[i][2], data.timestamps[i]) end end) function PLUGIN:AltPreFinalChecking(client) if (!IsValid(client)) then return end local data = client.ixAltData -- if we errored, don't do anything -- check will be reexecuted when the client rejoins if (data.error) then return end -- check if all queries finished data.checks = data.checks - 1 if (data.checks != 0) then return end -- cookie matches (STRONG) if (#data.otherCookies > 0) then for _, v in ipairs(data.otherCookies) do self:AltFound(client, v[2], "cookie", "cookie") data.discordAlert.cookies[#data.discordAlert.cookies + 1] = (v[2] or "").." ("..(v[1] or "")..")" data.discordAlert.altIDs[v[2]] = true end end -- IP (WEAK) if (#data.otherIPs > 0) then for _, v in ipairs(data.otherIPs) do data.discordAlert.ips[#data.discordAlert.ips + 1] = v[2] data.discordAlert.altIDs[v[2]] = true end end -- time matches (STRONG-MEDIUM) self:AggregateTimestampMatches(data) -- If no local cookie and player is known, check if a known cookie was time-matched if (data.localCookie == "" and data.mode == self.TYPE_KNOWN) then self:FindMatchingCookie(client, data) end if (#data.otherTimeMatches > 0) then -- go looking for the other clients that own our matched timestamps self:AltLookupCookieForTimestamps(client, data, #fileChecks) else -- else we don't need to wait for the lookup above data.checkComplete = true self:AltFinalChecking(client) end end function PLUGIN:AltFinalChecking(client) if (!IsValid(client)) then return end local data = client.ixAltData if (!data.checkComplete or data.altLogging != 0) then return end self:DiscordAlert(client) -- update IP list local steamID = client:SteamID64() local ip = self.GetIPAddress(client) local query = mysql:Select("bastion_antialt_userips") query:Where("steamid", steamID) query:Where("ip", ip) query:Callback(function(result) if (!result or #result == 0) then local query2 = mysql:Insert("bastion_antialt_userips") query2:Insert("steamid", steamID) query2:Insert("ip", ip) query2:Insert("last_seen", os.time()) query2:Execute() else local query2 = mysql:Update("bastion_antialt_userips") query2:Where("id", result[1].id) query2:Update("last_seen", os.time()) query2:Execute() end end) query:Execute() -- Kick if new player on proxy/vpn if (data.mode == self.TYPE_UNKNOWN) then if (self.API_KEY and (data.ip.proxy == "yes" or (data.ip.risk or 0) > 60)) then if (ix.config.Get("VPNKick")) then self:ProxyAlert(client) client:Kick(L("altSignupProxy", client)) else if (!self:NotifyProxyJoin(client) or ix.config.Get("ProxyAlwaysAlert")) then self:ProxyAlert(client) end end elseif (data.localCookie == "") then ix.log.Add(client, "altNewClient") self:GenerateCookie(client, data) else -- Update the cookie's timestamps self:StoreCookieInfo(data, fileChecks) -- Add this cookie to client as well so it can be time matched/timestamps updated self:StoreClientCookie(client, data) end elseif (data.localCookie == "") then ix.log.Add(client, "altKnownNewCookie") self:GenerateCookie(client, data) else -- Update the cookie's timestamps self:StoreCookieInfo(data, fileChecks) -- Add this cookie to client as well so it can be time matched/timestamps updated self:StoreClientCookie(client, data) end if (sam) then timer.Simple(3, function() if (!IsValid(client)) then return end local query1 = mysql:Select("bastion_antialt_alts") query1:Where("steamid", client:SteamID64()) query1:Select("alt_id") query1:Callback(function(result) if (!IsValid(client)) then return end if (!result or #result == 0) then return end local query2 = mysql:Select("bastion_antialt_alts") query2:Where("alt_id", result[1].alt_id) query2:WhereNotEqual("steamid", client:SteamID64()) query2:Select("steamid") query2:Callback(function(result2) if (!IsValid(client)) then return end if (!result2 or #result2 == 0) then return end for k, v in ipairs(result2) do sam.player.is_banned(util.SteamIDFrom64(v.steamid), function(banned) if (!banned or !IsValid(client)) then return end client:Kick("You have a banned alt account.") return end) end end) query2:Execute() end) query1:Execute() end) end end --[[ COOKIE STUFF ]] function PLUGIN:WhitelistPlayer(client) local data = client.ixAltData if (!data) then return end if (data.localCookie) then ix.log.Add(client, "altNewClient") self:GenerateCookie(client, data) return true end end function PLUGIN.RandomString() local result = {} -- The empty table we start with while (#result != 64) do local char = string.char(math.random(32, 126)) if (string.find(char, "%w")) then result[#result + 1] = char end end return table.concat(result) end function PLUGIN:GenerateCookie(client, data) local cookie = self.RandomString() self:UpdateLocalCookie(client, data, cookie) local query = mysql:Insert("bastion_antialt_users") query:Insert("steamid", client:SteamID64()) query:Insert("steam_name", client:SteamName()) query:Insert("cookie", cookie) query:Execute() end function PLUGIN:UpdateLocalCookie(client, data, cookie) data.localCookie = cookie net.Start("RecieveDupe") net.WriteUInt(2, 3) net.WriteString(COOKIE_KEY) net.WriteString(cookie) net.Send(client) self:StoreCookieInfo(data, fileChecks) end function PLUGIN:FindMatchingCookie(client, data) for _, v in ipairs(data.otherTimeMatches) do for _, v1 in ipairs(data.cookies) do if (v1.cookie == v[1]) then -- found a timestamp match belonging to the client, restore cookie -- in case of e.g. gmod reinstall ix.log.Add(client, "altKnownCookieMatched", v[2], #fileChecks) self:UpdateLocalCookie(client, data, v[1]) return end end end end --[[ LOGGING AND ALERTING ]] local ALT_OFFSET = 0 function PLUGIN:AltFound(client, steamid, type, info) local ids = {client:SteamID64(), steamid} local data = client.ixAltData data.altLogging = data.altLogging + 1 local query = mysql:Select("bastion_antialt_alts") query:WhereIn("steamid", ids) query:Callback(function(result) if (!result or #result == 0) then local alt_id = os.time() + ALT_OFFSET ALT_OFFSET = ALT_OFFSET + 1 local text = table.concat(ids,"+")..": "..info.." (alt-id: "..alt_id..")" self:InsertNewAlt(ids[1], alt_id, type, text) self:InsertNewAlt(steamid, alt_id, type, text) data.newAltFound = true elseif (#result == 1) then self:InsertNewAlt( result[1].steamid == steamid and ids[1] or steamid, result[1].alt_id, type, table.concat(ids,"+")..": "..info.." (alt-id: "..result[1].alt_id..")" ) data.newAltFound = true elseif (result[2].alt_id != result[1].alt_id) then self:MergeAlts( result[2].alt_id, result[1].alt_id, table.concat(ids,"+")..": "..info.." (alt-id: "..result[1].alt_id..")" ) data.newAltFound = true end data.altLogging = data.altLogging - 1 self:AltFinalChecking(client) end) query:Execute() end function PLUGIN:DiscordAlert(client) if (!self.DISCORD_WEBHOOK_ALTS or self.DISCORD_WEBHOOK_ALTS == "") then return end local data = client.ixAltData if (!data.newAltFound) then return end local tbl = { embeds = {{ title = "Alt found for "..client:SteamName(), description = "An alt account match was found for **"..client:SteamName().."** (*"..client:SteamID64().."*)\n\n__COOKIE__: 99.99% certainty via installed cookie\nShown as 'SteamID64 (SteamName)'\n__TIMESTAMP__: check via installation date/time or absense of mounted games (hl2,ep2,css,csgo,gmod)\nMore matches = more certainty, especially if all/most are installed\nShown as 'SteamID64 (SteamName; date/time matches - installed matches)'\n__IP__: users that connected from the same IP address at any point\nShown as 'SteamID64'", color = 13632027, timestamp = os.date("%Y-%m-%d %X%z"), footer = { text = "Bastion Alt Checker for GMod by Gr4Ss" }, author = { name = "Bastion Alt Checker" }, fields = { } }} } if (data.discordAlert.cookies and #data.discordAlert.cookies > 0) then table.insert(tbl.embeds[1].fields, {name = "COOKIE", value = table.concat(data.discordAlert.cookies, "\n")}) end if (data.discordAlert.timestamps and #data.discordAlert.timestamps > 0) then table.insert(tbl.embeds[1].fields, {name = "TIMESTAMP", value = table.concat(data.discordAlert.timestamps, "\n")}) end if (data.discordAlert.ips and #data.discordAlert.ips > 0) then table.insert(tbl.embeds[1].fields, { name = "IP", value = string.format("Address: [%s](https://proxycheck.io/threats/%s)\n", data.ipAddress, data.ipAddress).. table.concat(data.discordAlert.ips, "\n") }) end local ipLinks = {"["..client:SteamID64().."](https://steamidfinder.com/lookup/"..client:SteamID64().."/)"} for k in pairs(data.discordAlert.altIDs) do ipLinks[#ipLinks + 1] = "["..k.."](https://steamidfinder.com/lookup/"..k.."/)" end table.insert(tbl.embeds[1].fields, { name = "SteamID Finder Links", value = table.concat(ipLinks,"\n") }) for _, field in ipairs(tbl.embeds[1].fields) do if (string.len(field.value) > 1024) then field.value = string.sub(field.value, 1, 1024) end end local request = { failed = function(error) print("discord error", error) end, success = function(code, body, headers) if (code != 200) then print("discord error", code, body) end end, method = "post", url = self.DISCORD_WEBHOOK_ALTS, body = util.TableToJSON(tbl), type = "application/json; charset=utf-8" } CHTTP(request) end function PLUGIN:ProxyAlert(client) if (!self.DISCORD_WEBHOOK_ALTS or self.DISCORD_WEBHOOK_ALTS == "") then return end local ip, steamID = client.ixAltData.ipAddress, client:SteamID64() local tbl = { embeds = {{ title = "New player using VPN - "..client:SteamName(), description = client:SteamName().." joined WN for the first time, but was using a proxy/VPN. They have been kicked.\n\n".. string.format("More info: [%s](https://proxycheck.io/threats/%s) & [%s](https://steamidfinder.com/lookup/%s/)", ip, ip, steamID, steamID), color = 16312092, timestamp = os.date("%Y-%m-%d %X%z"), footer = { text = "Bastion Alt Checker for GMod by Gr4Ss" }, author = { name = "Bastion Alt Checker" } }} } local request = { failed = function(error) print("discord error", error) end, method = "post", url = self.DISCORD_WEBHOOK_ALTS, body = util.TableToJSON(tbl), type = "application/json; charset=utf-8" } CHTTP(request) end function PLUGIN:NotifyProxyJoin(client) local bSend = false for _, v in ipairs(player.GetAll()) do if (CAMI.PlayerHasAccess(v, "Helix - Proxy Notify")) then bSend = true v:NotifyLocalized("bastionProxyNotify", client:SteamName()) end end return bSend end