use version by @xdettlaff

This commit is contained in:
ghost 2023-12-26 18:58:55 +02:00
parent 75487e36e8
commit ba2acb1f71
1 changed files with 199 additions and 216 deletions

View File

@ -6,136 +6,85 @@ import traceback
import logging import logging
import os import os
from optparse import OptionParser from optparse import OptionParser
from struct import pack, unpack from struct import pack
from time import time from time import time
from ipaddress import ip_address, ip_network
from server_entry import ServerEntry from server_entry import ServerEntry
from protocol import MasterProtocol from protocol import MasterProtocol
import ipfilter
LOG_FILENAME = 'pymaster.log' LOG_FILENAME = "pymaster.log"
MAX_SERVERS_FOR_IP = 14 MAX_SERVERS_FOR_IP = 14
CHALLENGE_SEND_PERIOD = 10
def log(msg):
logging.debug(msg)
class RateLimitItem:
def __init__(self, resetAt):
self.reset(resetAt)
def reset(self, resetAt):
self.resetAt = resetAt
self.logs = self.calls = 0
def inc(self):
self.calls = self.calls + 1
self.logs = self.logs + 1
def shouldReset(self, curtime):
return curtime > self.resetAt
class IPRateLimit:
def __init__(self, type, period, maxcalls):
self.type = type
self.period = period
self.maxcalls = maxcalls
self.maxlogs = maxcalls + 2
self.ips = {}
def ratelimit(self, ip):
curtime = time()
if ip not in self.ips:
self.ips[ip] = RateLimitItem(curtime + self.period)
elif self.ips[ip].shouldReset(curtime):
self.ips[ip].reset(curtime + self.period)
self.ips[ip].inc()
if self.ips[ip].calls > self.maxcalls:
if self.ips[ip].logs < self.maxlogs:
log('Ratelimited %s %s' % (self.type, ip))
return True
return False
class PyMaster: class PyMaster:
def __init__(self, ip, port): def __init__(self, ip, port):
self.serverList = [] self.serverList = []
self.serverRL = IPRateLimit('server', 60, 30)
self.clientRL = IPRateLimit('client', 60, 120)
self.ipfilterRL = IPRateLimit('filterlog', 60, 10)
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sock.bind((ip, port)) self.sock.bind((ip, port))
log("Welcome to PyMaster!") logging.debug("Welcome to PyMaster!")
log("I ask you again, are you my master?") logging.debug("I ask you again, are you my master? @-@")
log("Running on %s:%d" % (ip, port)) logging.debug("Running on %s:%d" % (ip, port))
def serverLoop(self):
def server_loop(self):
data, addr = self.sock.recvfrom(1024) data, addr = self.sock.recvfrom(1024)
data = data.decode("latin_1")
if ip_address(addr[0]) in ipfilter.ipfilter: match data[0]:
if not self.ipfilterRL.ratelimit(addr[0]): case MasterProtocol.clientQuery:
log('Filter: %s:%d' % (addr[0], addr[1])) self.client_query(data, addr)
case MasterProtocol.challengeRequest:
self.send_challenge_to_server(data, addr)
case MasterProtocol.addServer:
self.add_server_to_list(data, addr)
case MasterProtocol.removeServer:
self.remove_server_from_list(data, addr)
case other:
logging.debug("Unknown message: {0} from {1}:{2}".format(data, addr[0], addr[1]))
def client_query(self, data, addr):
region = data[1] # UNUSED
data = data.strip("1" + region)
try:
query = data.split("\0")
except ValueError:
logging.debug(traceback.format_exc())
return return
if len(data) == 0: queryAddr = query[0] # UNUSED
return rawFilter = query[1]
# only client stuff # Remove first \ character
if data.startswith(MasterProtocol.clientQuery): rawFilter = rawFilter.strip("\\")
if not self.clientRL.ratelimit(addr[0]): split = rawFilter.split("\\")
self.clientQuery(data, addr)
return
# only server stuff # Use NoneType as undefined
if not self.serverRL.ratelimit(addr[0]): gamedir = "valve" # halflife, by default
if data.startswith(MasterProtocol.challengeRequest):
self.sendChallengeToServer(data, addr)
elif data.startswith(MasterProtocol.addServer):
self.addServerToList(data, addr)
elif data.startswith(MasterProtocol.removeServer):
self.removeServerFromList(data, addr)
else:
log('Unknown message: %s from %s:%d' % (str(data), addr[0], addr[1]))
def clientQuery(self, data, addr):
data = data.decode('latin_1')
data = data.strip('1' + data[1])
info = data.split('\0')[1].strip('\\')
split = info.split('\\')
protocol = None
gamedir = 'valve'
clver = None clver = None
nat = 0 nat = 0
for i in range(0, len(split), 2): for i in range(0, len(split), 2):
try: try:
k = split[i] key = split[i + 1]
v = split[i + 1] if split[i] == "gamedir":
if k == 'gamedir': gamedir = key.lower() # keep gamedir in lowercase
gamedir = v.lower() # keep gamedir in lowercase elif split[i] == "nat":
elif k == 'nat': nat = int(key)
nat = int(v) elif split[i] == "clver":
elif k == 'clver': clver = key
clver = v
elif k == 'protocol':
protocol = int(v)
# somebody is playing :)
elif k == 'thisismypcid' or k == 'heydevelopersifyoureadthis':
self.fakeInfoForOldVersions(gamedir, addr)
return
else: else:
log('Client Query: %s:%d, invalid infostring=%s' % (addr[0], addr[1], rawFilter)) logging.debug(
"Unhandled info string entry: {0}/{1}. Infostring was: {2}".format(
split[i], key, split
)
)
except IndexError: except IndexError:
pass pass
if( clver == None ): # Probably an old vulnerable version if clver is None: # Probably an old vulnerable version
self.fakeInfoForOldVersions(gamedir, addr) self.fake_info_for_old_versions(gamedir, addr)
return return
packet = MasterProtocol.queryPacketHeader packet = MasterProtocol.queryPacketHeader
@ -147,123 +96,157 @@ class PyMaster:
if not i.check: if not i.check:
continue continue
if nat != i.nat or gamedir != i.gamedir: if nat != i.nat:
continue continue
if protocol != None and protocol != i.protocol: if gamedir is not None and gamedir != i.gamedir:
continue continue
if nat: if nat:
reply = "\xff\xff\xff\xffc {0}:{1}".format(addr[0], addr[1])
data = reply.encode("latin_1")
# Tell server to send info reply # Tell server to send info reply
data = ('\xff\xff\xff\xffc %s:%d' % (addr[0], addr[1])).encode('latin_1')
self.sock.sendto(data, i.addr) self.sock.sendto(data, i.addr)
# Use pregenerated address string # Use pregenerated address string
packet += i.queryAddr packet += i.queryAddr
packet += b'\0\0\0\0\0\0' # Fill last IP:Port with \0
packet += b"\0\0\0\0\0\0" # Fill last IP:Port with \0
self.sock.sendto(packet, addr) self.sock.sendto(packet, addr)
def fakeInfoForOldVersions(self, gamedir, addr):
def sendFakeInfo(sock, warnmsg, gamedir, addr): def _send_fake_info(sock, warnmsg, gamedir, addr):
baseReply = b"\xff\xff\xff\xffinfo\n\host\\" + warnmsg.encode('utf-8') + b"\map\\update\dm\\0\\team\\0\coop\\0\\numcl\\32\maxcl\\32\\gamedir\\" + gamedir.encode('latin-1') + b"\\" baseReply = (
b"\xff\xff\xff\xffinfo\n\host\\"
+ warnmsg.encode("utf-8")
+ b"\map\\update\dm\\0\\team\\0\coop\\0\\numcl\\32\maxcl\\32\\gamedir\\"
+ gamedir.encode("latin-1")
+ b"\\"
)
sock.sendto(baseReply, addr) sock.sendto(baseReply, addr)
sendFakeInfo(self.sock, "This version is not", gamedir, addr)
sendFakeInfo(self.sock, "supported anymore", gamedir, addr)
sendFakeInfo(self.sock, "Please update Xash3DFWGS", gamedir, addr)
sendFakeInfo(self.sock, "From GooglePlay or GitHub", gamedir, addr)
sendFakeInfo(self.sock, "Эта версия", gamedir, addr)
sendFakeInfo(self.sock, "устарела", gamedir, addr)
sendFakeInfo(self.sock, "Обновите Xash3DFWGS c", gamedir, addr)
sendFakeInfo(self.sock, "GooglePlay или GitHub", gamedir, addr)
def removeServerFromList(self, data, addr): def fake_info_for_old_versions(self, gamedir, addr):
pass error_message = [
"This version is not",
"supported anymore",
"Please update Xash3DFWGS",
"From GooglePlay or GitHub",
"Эта версия",
"устарела",
"Обновите Xash3DFWGS c",
"GooglePlay или GitHub",
]
for string in error_message:
_send_fake_info(self.sock, string, gamedir, addr)
def remove_server_from_list(self, addr):
for server in self.serverList:
if server.addr == addr:
logging.debug("Remove Server: from {0}:{1}".format(addr[0], addr[1]))
self.serverList.remove(server)
def send_challenge_to_server(self, addr):
logging.debug("Challenge Request: from {0}:{1}".format(addr[0], addr[1]))
# At first, remove old server- data from list
# self.removeServerFromList(None, addr)
def sendChallengeToServer(self, data, addr):
count = 0 count = 0
s = None
for i in self.serverList: for i in self.serverList:
if addr[0] != i.addr[0]: if i.addr[0] == addr[0]:
continue if i.addr[1] == addr[1]:
if addr[1] == i.addr[1]: self.serverList.remove(i)
s = i
break
else: else:
count += 1 count += 1
if count > MAX_SERVERS_FOR_IP: if count > MAX_SERVERS_FOR_IP:
return return
challenge2 = None challenge = random.randint(0, 2**32 - 1)
if len(data) == 6:
# little endian challenge
challenge2 = unpack('<I', data[2:])[0]
if not s: # Add server to list
challenge = random.randint(0, 2**32-1) & 0xffffffff #hash(addr[0]) + hash(addr[1]) + hash(time()) self.serverList.append(ServerEntry(addr, challenge))
s = ServerEntry(addr, challenge)
self.serverList.append(s)
elif s.sentChallengeAt + 5 > time():
return
# And send him a challenge
packet = MasterProtocol.challengePacketHeader packet = MasterProtocol.challengePacketHeader
packet += pack('I', s.challenge) packet += pack("I", challenge)
# send server-to-master challenge back
if challenge2 is not None:
packet += pack('I', challenge2)
self.sock.sendto(packet, addr) self.sock.sendto(packet, addr)
def addServerToList(self, data, addr):
def add_server_to_list(self, data, addr):
logging.debug("Add Server: from {0}:{1}".format(addr[0], addr[1]))
# Remove the header. Just for better parsing. # Remove the header. Just for better parsing.
info = data.strip(b'\x30\x0a\x5c').decode('latin_1') serverInfo = data.strip("\x30\x0a\x5c")
# Find a server with same address # Find a server with same address
s = None for serverEntry in self.serverList:
for s in self.serverList: if serverEntry.addr == addr:
if s.addr == addr:
break break
if not s:
log('Server skipped challenge request: %s:%d' % (addr[0], addr[1]))
return
if s.setInfoString( info ): serverEntry.setInfoString(serverInfo)
log('Add server: %s:%d, game=%s/%s, protocol=%d, players=%d/%d/%d, version=%s' % (addr[0], addr[1], s.gamemap, s.gamedir, s.protocol, s.players, s.bots, s.maxplayers, s.version))
else:
log('Failed challenge from %s:%d: %d must be %d' % (addr[0], addr[1], s.challenge, s.challenge2))
def spawn_pymaster(verbose, ip, port): def spawn_pymaster(verbose, ip, port):
if verbose: if verbose:
logging.getLogger().addHandler(logging.StreamHandler()) logging.getLogger().addHandler(logging.StreamHandler())
# logging.getLogger().addHandler(logging.FileHandler(LOG_FILENAME)) logging.getLogger().addHandler(logging.FileHandler(LOG_FILENAME))
logging.getLogger().setLevel(logging.DEBUG) logging.getLogger().setLevel(logging.DEBUG)
masterMain = PyMaster(ip, port) masterMain = PyMaster(ip, port)
while True: while True:
try: try:
masterMain.serverLoop() masterMain.server_loop()
except Exception: except Exception:
log(traceback.format_exc()) logging.debug(traceback.format_exc())
pass
if __name__ == "__main__": if __name__ == "__main__":
parser = OptionParser() parser = OptionParser()
parser.add_option('-i', '--ip', action='store', dest='ip', default='0.0.0.0', parser.add_option(
help='ip to listen [default: %default]') "-i",
parser.add_option('-p', '--port', action='store', dest='port', type='int', default=27010, "--ip",
help='port to listen [default: %default]') action="store",
parser.add_option('-d', '--daemonize', action='store_true', dest='daemonize', default=False, dest="ip",
help='run in background, argument is uid [default: %default]') default="0.0.0.0",
parser.add_option('-q', '--quiet', action='store_false', dest='verbose', default=True, help="ip to listen [default: %default]",
help='don\'t print to stdout [default: %default]') )
parser.add_option(
"-p",
"--port",
action="store",
dest="port",
type="int",
default=27010,
help="port to listen [default: %default]",
)
parser.add_option(
"-d",
"--daemonize",
action="store_true",
dest="daemonize",
default=False,
help="run in background, argument is uid [default: %default]",
)
parser.add_option(
"-q",
"--quiet",
action="store_false",
dest="verbose",
default=True,
help="don't print to stdout [default: %default]",
)
(options, args) = parser.parse_args() (options, args) = parser.parse_args()
if options.daemonize != 0: if options.daemonize != 0:
from daemon import pidfile, DaemonContext from daemon import DaemonContext
with DaemonContext(stdout=sys.stdout, stderr=sys.stderr, working_directory=os.getcwd()) as context: with DaemonContext(
stdout=sys.stdout, stderr=sys.stderr, working_directory=os.getcwd()
) as context:
spawn_pymaster(options.verbose, options.ip, options.port) spawn_pymaster(options.verbose, options.ip, options.port)
else: else:
sys.exit(spawn_pymaster(options.verbose, options.ip, options.port)) sys.exit(spawn_pymaster(options.verbose, options.ip, options.port))