diff --git a/fail2ban-p2p/command.py b/fail2ban-p2p/command.py index bef56cf..cbc18ba 100644 --- a/fail2ban-p2p/command.py +++ b/fail2ban-p2p/command.py @@ -1,8 +1,17 @@ +# Copyright 2013 Johannes Fuermann +# Copyright 2013 Manuel Munz +# +# This file is part of fail2ban-p2p. +# +# Licensed under the GNU GENERAL PUBLIC LICENSE Version 3. For details +# see the file COPYING or http://www.gnu.org/licenses/gpl-3.0.en.html. + import json -import M2Crypto +from M2Crypto import EVP import config +import crypto import log import util import version @@ -11,7 +20,9 @@ logger = log.initialize_logging("fail2ban-p2p." + __name__) class Command: - """Handle command objects.""" + """ + Handle command objects. + """ msgType = None parameter = () @@ -26,9 +37,12 @@ class Command: self.hops = hops if hops is not None else [] def __string__(self): - return f"Command (msgType = {self.msgType}, ...)" + return "Command (msgType = " + str(self.msgType) + ", ...)" def toSerializableDict(self): + """ + Returns a recursively sorted dictionary. + """ unordered_dict = { "msgType": self.msgType, "parameter": self.parameter, @@ -37,6 +51,9 @@ class Command: return util.sort_recursive(unordered_dict) def toProtocolMessage(self): + """ + Create a JSON-encoded protocol message. + """ serializable_dict = self.toSerializableDict() signed_message = json.dumps(serializable_dict) signature = self.sign(signed_message) @@ -48,10 +65,19 @@ class Command: return json.dumps(signed_dict) def sign(self, text): + """ + Compute signature for a message. + + Args: + text (str): The JSON encoded message text. + + Returns: + str: Hex-encoded signature. + """ logger.debug("signing outgoing message") c = config.Config() - signer = M2Crypto.EVP.load_key(c.privkey) + signer = EVP.load_key(c.privkey) signer.sign_init() signer.sign_update(text.encode("utf-8")) string_signature = signer.sign_final().hex() diff --git a/fail2ban-p2p/crypto.py b/fail2ban-p2p/crypto.py index e119900..66c92b3 100644 --- a/fail2ban-p2p/crypto.py +++ b/fail2ban-p2p/crypto.py @@ -1,7 +1,14 @@ -import os -import sys +# Copyright 2013 Johannes Fuermann +# Copyright 2013 Manuel Munz +# +# This file is part of fail2ban-p2p. +# +# Licensed under the GNU GENERAL PUBLIC LICENSE Version 3. For details +# see the file COPYING or http://www.gnu.org/licenses/gpl-3.0.en.html. -import M2Crypto +import os + +from M2Crypto import Rand, RSA import config import log @@ -12,14 +19,16 @@ logger = log.initialize_logging("fail2ban-p2p." + __name__) def create_keys(): """Create private/public keypair (RSA 1024 bit).""" + if os.path.isfile(c.privkey) or os.path.isfile(c.pubkey): print("A keypair for this node already exists.") - ask = input('Do you really want to create a new one? [y/N] ') + ask = input("Do you really want to create a new one? [y/N] ") if ask != "y": return - M2Crypto.Rand.rand_seed(os.urandom(1024)) + + Rand.rand_seed(os.urandom(1024)) logger.info("Generating a 1024 bit private/public key pair...") - keypair = M2Crypto.RSA.gen_key(1024, 65537) + keypair = RSA.gen_key(1024, 65537) try: keypair.save_key(c.privkey, None) os.chmod(c.privkey, 0o400) @@ -28,4 +37,4 @@ def create_keys(): logger.debug("Public key was saved to %s", c.pubkey) except IOError as e: logger.error("Could not save the keypair, check permissions! %s", e) - sys.exit(1) + raise diff --git a/fail2ban-p2p/node.py b/fail2ban-p2p/node.py index 7004385..dbfa0ab 100644 --- a/fail2ban-p2p/node.py +++ b/fail2ban-p2p/node.py @@ -1,3 +1,11 @@ +# Copyright 2013 Johannes Fuermann +# Copyright 2013 Manuel Munz +# +# This file is part of fail2ban-p2p. +# +# Licensed under the GNU GENERAL PUBLIC LICENSE Version 3. For details +# see the file COPYING or http://www.gnu.org/licenses/gpl-3.0.en.html. + import hashlib import json import os @@ -7,7 +15,7 @@ import threading from select import select from time import time -import M2Crypto +from M2Crypto import EVP, RSA import config import crypto @@ -47,22 +55,26 @@ class Node: logger.debug("running version: %s", version.version) try: sockets = [] - for a in self.addresses: + for address in self.addresses: try: - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.bind((a, int(self.port))) - s.listen(1) - sockets.append(s) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.bind((address, int(self.port))) + sock.listen(1) + sockets.append(sock) except Exception as e: - logger.warning("Couldn't bind to address %s (Reason: %s)", a, e) + logger.warning("Couldn't bind to address %s (Reason: %s)", address, e) while self.running: readable, _, _ = select(sockets, [], []) for sock in readable: client_socket, address = sock.accept() logger.debug("connection from %s", address[0]) - t = threading.Thread(target=server.serve, args=(self, client_socket, address), daemon=True) - t.start() + thread = threading.Thread( + target=server.serve, + args=(self, client_socket, address), + daemon=True, + ) + thread.start() except Exception as e: print(e) @@ -73,84 +85,101 @@ class Node: def processMessages(self): self.lock.acquire() logger.debug("begin message handling") - for c in self.messageQueue: - if self.uid not in c.hops: - if c.hops[0] == 'local': - del c.hops[0] - c.hops.append(self.uid) - if c.msgType == 1: - if 'Trustlevel' not in c.parameter: - logger.warning("Incoming Message has no Trustlevel, I won't trust it. Never.") - c.parameter['Trustlevel'] = 0 + for command in self.messageQueue: + if self.uid not in command.hops: + if command.hops[0] == "local": + del command.hops[0] + command.hops.append(self.uid) + if command.msgType == 1: + if "Trustlevel" not in command.parameter: + logger.warning("Incoming message has no Trustlevel, I won't trust it.") + command.parameter["Trustlevel"] = 0 - if c.sender != "local": - c.parameter['Trustlevel'] = int((float(c.sender.trustLevel) / 100 * float(c.parameter['Trustlevel']) / 100) * 100) - logger.debug("Message now has trust level %s", c.parameter['Trustlevel']) + if command.sender != "local": + command.parameter["Trustlevel"] = int( + (float(command.sender.trustLevel) / 100.0) + * (float(command.parameter["Trustlevel"]) / 100.0) + * 100 + ) + logger.debug("Message now has trust level %s", command.parameter["Trustlevel"]) relay = True ipindb = False - if len(self.banList) > 0: + if self.banList: for ban in self.banList: - if ban['AttackerIP'] == c.parameter['AttackerIP']: + if ban["AttackerIP"] == command.parameter["AttackerIP"]: ipindb = True logger.debug("IP already in database.") - if int(ban['Timestamp']) != int(c.parameter['Timestamp']): - if c.hops[0] not in ban['Hops']: - trustold = ban['Trustlevel'] - trustnew = int(trustold) + int(c.parameter['Trustlevel']) + if int(ban["Timestamp"]) != int(command.parameter["Timestamp"]): + if command.hops[0] not in ban["Hops"]: + trustold = ban["Trustlevel"] + trustnew = int(trustold) + int(command.parameter["Trustlevel"]) if trustnew > 100: trustnew = 100 - ban['Trustlevel'] = trustnew - ban['Hops'].append(c.hops[0]) + ban["Trustlevel"] = trustnew + ban["Hops"].append(command.hops[0]) logger.debug("TrustLevel for this IP is now %s", trustnew) - c.parameter['Trustlevel'] = trustnew + command.parameter["Trustlevel"] = trustnew else: relay = False - logger.debug("There is already an entry from %s in our database, do nothing with this message.", c.hops[0]) + logger.debug( + "There is already an entry from %s in our database, do nothing with this message.", + command.hops[0], + ) else: relay = False logger.debug("Timestamp has not changed, do nothing with this message") if not ipindb: - self.banList.append({ - 'AttackerIP': c.parameter['AttackerIP'], - 'Timestamp': c.parameter['Timestamp'], - 'BanTime': self.banTime, - 'Trustlevel': c.parameter['Trustlevel'], - 'Hops': [c.hops[0]], - }) - logger.debug("Added %s to internal banlist", c.parameter['AttackerIP']) + self.banList.append( + { + "AttackerIP": command.parameter["AttackerIP"], + "Timestamp": command.parameter["Timestamp"], + "BanTime": self.banTime, + "Trustlevel": command.parameter["Trustlevel"], + "Hops": [command.hops[0]], + } + ) + logger.debug("Added %s to internal banlist", command.parameter["AttackerIP"]) if relay: - if int(c.parameter['Trustlevel']) >= int(config.Config().threshold): - logger.ban(c.parameter['AttackerIP']) + if int(command.parameter["Trustlevel"]) >= int(config.Config().threshold): + logger.ban(command.parameter["AttackerIP"]) else: - logger.debug("Message's trust level (%s) was below our threshold (%s)", c.parameter['Trustlevel'], config.Config().threshold) + logger.debug( + "Message's trust level (%s) was below our threshold (%s)", + command.parameter["Trustlevel"], + config.Config().threshold, + ) for peer in self.friends: logger.debug("sending message to all friends") - peer.sendCommand(c) + peer.sendCommand(command) - if c.msgType == 3: - sender_uid = c.hops[0] + if command.msgType == 3: + sender_uid = command.hops[0] for peer in self.friends: - logger.debug("Comparing senders uid (%s) with one of our friends uid (%s)", sender_uid, peer.uid) + logger.debug( + "Comparing senders uid (%s) with one of our friends uid (%s)", + sender_uid, + peer.uid, + ) if peer.uid == sender_uid: logger.debug("The message is from our friend %s (uid: %s)", peer.name, peer.uid) logger.debug("Dumping banlist to %s (uid: %s)", peer.name, peer.uid) - if len(self.banList) > 0: + if self.banList: for ban in self.banList: - cmd = Command() - cmd.msgType = 1 - cmd.hops = [self.uid] - cmd.protocolVersion = version.protocolVersion - cmd.parameter = { - "AttackerIP": ban['AttackerIP'], - "Timestamp": ban['Timestamp'], - "Trustlevel": ban['Trustlevel'], + dump_cmd = Command() + dump_cmd.msgType = 1 + dump_cmd.hops = [self.uid] + dump_cmd.protocolVersion = version.protocolVersion + dump_cmd.parameter = { + "AttackerIP": ban["AttackerIP"], + "Timestamp": ban["Timestamp"], + "Trustlevel": ban["Trustlevel"], } - peer.sendCommand(cmd) + peer.sendCommand(dump_cmd) else: logger.debug("I know this message, I won't resend it to prevent loops") logger.debug("end message handling") @@ -170,9 +199,12 @@ class Node: self.configPath = c.configPath self.configFile = c.configFile - with open(c.pubkey, 'r', encoding='utf-8') as fh: - pubkey_file = fh.read() - pubkey = re.findall("-----BEGIN PUBLIC KEY-----(.*?)-----END PUBLIC KEY-----", pubkey_file, re.DOTALL | re.M)[0] + pubkey_file = open(c.pubkey, "r", encoding="utf-8").read() + pubkey = re.findall( + "-----BEGIN PUBLIC KEY-----(.*?)-----END PUBLIC KEY-----", + pubkey_file, + re.DOTALL | re.M, + )[0] logger.debug("our own pubkey is: %s", pubkey) @@ -186,28 +218,36 @@ class Node: def getFriends(self): error = False - friendPath = os.path.join(self.configPath, 'friends') - friends = [f for f in os.listdir(friendPath) if os.path.isfile(os.path.join(friendPath, f))] - if not friends: + friendPath = os.path.join(self.configPath, "friends") + friend_files = [f for f in os.listdir(friendPath) if os.path.isfile(os.path.join(friendPath, f))] + if not friend_files: logger.warning("No friends found. In order to properly use fail2ban-p2p add at least one friend.") - for file in friends: - with open(os.path.join(friendPath, file), 'r', encoding='utf-8') as f: - friendinfo = f.read() + for file in friend_files: + with open(os.path.join(self.configPath, "friends", file), "r", encoding="utf-8") as handle: + friendinfo = str(handle.read()) + pubkey = None try: - pubkey = re.findall("-----BEGIN PUBLIC KEY-----(.*?)-----END PUBLIC KEY-----", friendinfo, re.DOTALL | re.M)[0] + pubkey = re.findall( + "-----BEGIN PUBLIC KEY-----(.*?)-----END PUBLIC KEY-----", + friendinfo, + re.DOTALL | re.M, + )[0] except IndexError: logger.warning("No pubkey found in config for %s", file) error = True + if pubkey: logger.debug("read friend's public key: %s", pubkey) uid = hashlib.sha224(pubkey.encode("utf-8")).hexdigest() + try: address = re.search(r"address\s*=\s*(.*)", friendinfo).group(1) except AttributeError: logger.warning("address not found in config for %s", file) error = True + try: port = re.search(r"port\s*=\s*(.*)", friendinfo).group(1) if not 0 < int(port) < 65536: @@ -216,6 +256,7 @@ class Node: except AttributeError: logger.warning("port not found in config for %s", file) error = True + try: trustlevel = re.search(r"trustlevel\s*=\s*(.*)", friendinfo).group(1) except AttributeError: @@ -223,19 +264,32 @@ class Node: error = True if not error: - obj = friend.Friend(name=file, uid=uid, address=address, port=int(port), trustLevel=int(trustlevel), publicKey=pubkey) - obj.configpath = os.path.join(friendPath, file) - logger.debug("added friend %s (uid=%s, address=%s, port=%s, trustLevel=%s)", file, uid, address, port, trustlevel) + obj = friend.Friend( + name=file, + uid=uid, + address=address, + port=int(port), + trustLevel=int(trustlevel), + publicKey=pubkey, + ) + obj.configpath = os.path.join(self.configPath, "friends", file) + logger.debug( + "added friend %s (uid=%s, address=%s, port=%s, trustLevel=%s)", + file, + uid, + address, + port, + trustlevel, + ) self.friends.append(obj) else: logger.error("Could not add friend '%s' due to errors in the config file", file) - error = False def verifyMessage(self, message): logger.debug("signature in command class is: %s", message.signature) logger.debug("attempting to verify command") - if message.msgType is None: + if not message.msgType: logger.warning("Required parameter 'msgType' is missing in received message.") raise customexceptions.InvalidMessage if not validators.isInteger(message.msgType): @@ -254,8 +308,8 @@ class Node: logger.warning("Signature is missing in received message") raise customexceptions.InvalidMessage - for h in message.hops: - if not validators.isAlphaNumeric(h): + for hop in message.hops: + if not validators.isAlphaNumeric(hop): logger.warning("Invalid characters in hops. Only alphanumeric characters are allowed.") raise customexceptions.InvalidMessage @@ -267,29 +321,32 @@ class Node: if "AttackerIP" not in message.parameter: logger.warning("Required parameter 'AttackerIP' is missing in received message.") raise customexceptions.InvalidMessage - if not validators.isIPv4address(message.parameter['AttackerIP']): + if not validators.isIPv4address(message.parameter["AttackerIP"]): logger.warning('Invalid parameter "AttackerIP" in received message.') raise customexceptions.InvalidMessage if "Timestamp" not in message.parameter: logger.warning("Required parameter 'Timestamp' is missing in received message.") raise customexceptions.InvalidMessage - if not validators.isInteger(message.parameter['Timestamp']): + if not validators.isInteger(message.parameter["Timestamp"]): logger.warning('Invalid parameter "Timestamp" in received message.') raise customexceptions.InvalidMessage - if 'Trustlevel' not in message.parameter: + if "Trustlevel" not in message.parameter: logger.warning('Required parameter "Trustlevel" in missing in received message.') raise customexceptions.InvalidMessage - if not (validators.isInteger(message.parameter['Trustlevel']) and 0 <= int(message.parameter['Trustlevel']) <= 100): + if not ( + validators.isInteger(message.parameter["Trustlevel"]) + and 0 <= int(message.parameter["Trustlevel"]) <= 100 + ): logger.warning('Invalid parameter "Trustlevel" in received message.') raise customexceptions.InvalidMessage - elif message.msgType == 2 or message.msgType == 3: + elif message.msgType in (2, 3): if "TimeFrame" not in message.parameter: logger.warning("Required parameter 'TimeFrame' is missing in received message.") raise customexceptions.InvalidMessage - if not validators.isInteger(message.parameter['TimeFrame']): + if not validators.isInteger(message.parameter["TimeFrame"]): logger.warning('Invalid parameter "TimeFrame" in received message.') raise customexceptions.InvalidMessage else: @@ -301,32 +358,35 @@ class Node: logger.debug("Last hop's uid is: %s", last_hop_uid) sender = None + pk = None for peer in self.friends: logger.debug("Comparing last hops uid (%s) with one of our friends uid (%s)", last_hop_uid, peer.uid) if peer.uid == last_hop_uid: logger.debug("The message seems to be from our friend %s (uid: %s)", peer.name, peer.uid) sender = peer - pk = M2Crypto.RSA.load_pub_key(sender.configpath) + pk = RSA.load_pub_key(sender.configpath) break - if last_hop_uid == "local": - logger.debug("This message was signed with our own key.") - c = config.Config() - pk = M2Crypto.RSA.load_pub_key(c.pubkey) - sender = "local" - break - if sender is None: + + if last_hop_uid == "local": + logger.debug("This message was signed with our own key.") + c = config.Config() + pk = RSA.load_pub_key(c.pubkey) + sender = "local" + + if sender is None or pk is None: logger.warning("The message could not be mapped to one of our friends!") raise customexceptions.InvalidSignature - verifier = M2Crypto.EVP.PKey() + verifier = EVP.PKey() verifier.assign_rsa(pk) verifier.verify_init() verifier.verify_update(json.dumps(message.toSerializableDict()).encode("utf-8")) if verifier.verify_final(bytes.fromhex(message.signature)) != 1: - logger.warning('Signature doesnt match!') + logger.warning("Signature doesnt match!") return False - logger.debug('Signature verified successfully') + + logger.debug("Signature verified successfully") message.sender = sender return True @@ -338,7 +398,7 @@ class Node: timeframestart = int(time()) - int(timeframe) logger.debug("Dumping all nodes that were inserted after %s", timeframestart) for entry in self.banList: - if int(entry['Timestamp']) > int(timeframestart): + if int(entry["Timestamp"]) > int(timeframestart): banlist.append(entry) return json.dumps(banlist) @@ -346,22 +406,22 @@ class Node: def requestBanlist(self): for peer in self.friends: logger.debug("Sending dump request to %s", peer.name) - c = Command() - c.msgType = 3 - c.hops = [self.uid] - c.protocolVersion = version.protocolVersion - c.parameter = {"TimeFrame": self.banTime or 3600} - peer.sendCommand(c) + command = Command() + command.msgType = 3 + command.hops = [self.uid] + command.protocolVersion = version.protocolVersion + command.parameter = {"TimeFrame": self.banTime or 3600} + peer.sendCommand(command) def cleanBanlist(self): logger.debug("Purging all entries from banlist that are older than %s seconds.", self.banTime) - if len(self.banList) > 0: + if self.banList: banListKeep = [] for ban in self.banList: - if ban['Timestamp'] + self.banTime > time(): + if ban["Timestamp"] + self.banTime > time(): banListKeep.append(ban) else: - logger.info("Removed %s from internal banlist because the ban has expired.", ban['AttackerIP']) + logger.info("Removed %s from internal banlist because the ban has expired.", ban["AttackerIP"]) self.banList = banListKeep global cleaner cleaner = threading.Timer(60, self.cleanBanlist)