Browse Source

Merge branch 'improvements2' of stefan.schmidt/ID2T-toolkit into master

Carlos Garcia 6 years ago
parent
commit
99daa6cf2e

+ 18 - 19
code/Attack/BaseAttack.py

@@ -12,9 +12,8 @@ import sys
 import tempfile
 import time
 import collections
+import typing as t
 
-# TODO: double check this import
-# does it complain because libpcapreader is not a .py?
 import ID2TLib.libpcapreader as pr
 import lea
 import numpy as np
@@ -97,7 +96,7 @@ class BaseAttack(metaclass=abc.ABCMeta):
     ################################################
 
     @staticmethod
-    def _is_mac_address(mac_address: str):
+    def _is_mac_address(mac_address: t.Union[str, t.List[str]]) -> bool:
         """
         Verifies if the given string is a valid MAC address.
         Accepts the formats 00:80:41:ae:fd:7e and 00-80-41-ae-fd-7e.
@@ -117,7 +116,7 @@ class BaseAttack(metaclass=abc.ABCMeta):
         return True
 
     @staticmethod
-    def _is_ip_address(ip_address: str):
+    def _is_ip_address(ip_address: t.Union[str, t.List[str]]) -> t.Tuple[bool, t.Union[str, t.List[str]]]:
         """
         Verifies that the given string or list of IP addresses (strings) is a valid IPv4/IPv6 address.
         Accepts comma-separated lists of IP addresses, like "192.169.178.1, 192.168.178.2"
@@ -126,7 +125,7 @@ class BaseAttack(metaclass=abc.ABCMeta):
         :return: True if all IP addresses are valid, otherwise False. And a list of IP addresses as string.
         """
 
-        def append_ips(ip_address_input):
+        def append_ips(ip_address_input: t.List[str]) -> t.Tuple[bool, t.List[str]]:
             """
             Recursive appending function to handle lists and ranges of IP addresses.
 
@@ -161,7 +160,8 @@ class BaseAttack(metaclass=abc.ABCMeta):
             return result, ip_address_output
 
     @staticmethod
-    def _is_port(ports_input: str):
+    def _is_port(ports_input: t.Union[t.List[str], t.List[int], str, int])\
+            -> t.Union[bool, t.Tuple[bool, t.List[t.Union[int, str]]]]:
         """
         Verifies if the given value is a valid port. Accepts port ranges, like 80-90, 80..99, 80...99.
 
@@ -171,7 +171,7 @@ class BaseAttack(metaclass=abc.ABCMeta):
         and a list of int is returned.
         """
 
-        def _is_invalid_port(num):
+        def _is_invalid_port(num: int) -> bool:
             """
             Checks whether the port number is invalid.
 
@@ -224,7 +224,7 @@ class BaseAttack(metaclass=abc.ABCMeta):
             return True, ports_output
 
     @staticmethod
-    def _is_timestamp(timestamp: str):
+    def _is_timestamp(timestamp: str) -> bool:
         """
         Checks whether the given value is in a valid timestamp format. The accepted format is:
         YYYY-MM-DD h:m:s, whereas h, m, s may be one or two digits.
@@ -276,7 +276,7 @@ class BaseAttack(metaclass=abc.ABCMeta):
             return False, value
 
     @staticmethod
-    def _is_domain(val: str):
+    def _is_domain(val: str) -> bool:
         """
         Verifies that the given string is a valid URI.
 
@@ -309,25 +309,25 @@ class BaseAttack(metaclass=abc.ABCMeta):
             random.seed(seed_final)
             np.random.seed(seed_final & 0xFFFFFFFF)
 
-    def set_start_time(self):
+    def set_start_time(self) -> None:
         """
         Set the current time as global starting time.
         """
         self.start_time = time.time()
 
-    def set_finish_time(self):
+    def set_finish_time(self) -> None:
         """
         Set the current time as global finishing time.
         """
         self.finish_time = time.time()
 
-    def get_packet_generation_time(self):
+    def get_packet_generation_time(self) -> float:
         """
         :return difference between starting and finishing time.
         """
         return self.finish_time - self.start_time
 
-    def add_param_value(self, param, value, user_specified: bool = True):
+    def add_param_value(self, param, value, user_specified: bool = True) -> None:
         """
         Adds the pair param : value to the dictionary of attack parameters. Prints and error message and skips the
         parameter if the validation fails.
@@ -411,19 +411,18 @@ class BaseAttack(metaclass=abc.ABCMeta):
         elif param_type == atkParam.ParameterTypes.TYPE_PERCENTAGE:
             is_valid_float, value = self._is_float(value)
             if is_valid_float:
-                is_valid = value >= 0 and value <= 1
+                is_valid = 0 <= value <= 1
             else:
                 is_valid = False
         elif param_type == atkParam.ParameterTypes.TYPE_PADDING:
             if isinstance(value, int):
-                is_valid = value >= 0 and value <= 100
+                is_valid = 0 <= value <= 100
             elif isinstance(value, str) and value.isdigit():
                 value = int(value)
-                is_valid = value >= 0 and value <= 100
+                is_valid = 0 <= value <= 100
         elif param_type == atkParam.ParameterTypes.TYPE_INTERVAL_SELECT_STRAT:
             is_valid = value in {"random", "optimal", "custom"}
 
-
         # add value iff validation was successful
         if is_valid:
             self.params[param_name] = self.ValuePair(value, user_specified)
@@ -501,7 +500,7 @@ class BaseAttack(metaclass=abc.ABCMeta):
 
         return destination
 
-    def get_reply_delay(self, ip_dst, default = 2000):
+    def get_reply_delay(self, ip_dst, default=2000):
         """
            Gets the minimum and the maximum reply delay for all the connections of a specific IP.
 
@@ -522,7 +521,7 @@ class BaseAttack(metaclass=abc.ABCMeta):
             all_max_delays = self.statistics.process_db_query("SELECT maxDelay FROM conv_statistics LIMIT 500;")
             max_delay = np.median(all_max_delays)
 
-            if math.isnan(min_delay): # max_delay is nan too then
+            if math.isnan(min_delay):  # max_delay is nan too then
                 if default < 0:
                     raise ValueError("Could not calculate min/max_delay")
 

+ 1 - 1
code/Attack/FTPWinaXeExploit.py

@@ -12,7 +12,7 @@ logging.getLogger("scapy.runtime").setLevel(logging.ERROR)
 
 # noinspection PyPep8
 
-#FTP Port used for Creation of packets
+# FTP Port used for Creation of packets
 ftp_port = 21
 
 

+ 18 - 9
code/ID2TLib/Botnet/CommunicationProcessor.py

@@ -4,12 +4,12 @@ from Attack.MembersMgmtCommAttack import MessageType
 from Attack.MembersMgmtCommAttack import Message
 
 
-class CommunicationProcessor():
+class CommunicationProcessor:
     """
     Class to process parsed input CSV/XML data and retrieve a mapping or other information.
     """
 
-    def __init__(self, mtypes:dict, nat:bool):
+    def __init__(self, mtypes: dict, nat: bool):
         """
         Creates an instance of CommunicationProcessor.
         :param mtypes: a dict containing an int to EnumType mapping of MessageTypes
@@ -18,6 +18,12 @@ class CommunicationProcessor():
         self.packets = []
         self.mtypes = mtypes
         self.nat = nat
+        self.messages = []
+        self.respnd_ids = set()
+        self.external_init_ids = set()
+        self.local_init_ids = dict()
+        self.local_ids = dict()
+        self.external_ids = set()
 
     def set_mapping(self, packets: list, mapped_ids: dict):
         """
@@ -29,7 +35,9 @@ class CommunicationProcessor():
         self.packets = packets
         self.local_init_ids = set(mapped_ids)
 
-    def get_comm_interval(self, cpp_comm_proc, strategy: str, number_ids: int, max_int_time: int, start_idx: int, end_idx: int):
+    @staticmethod
+    def get_comm_interval(cpp_comm_proc, strategy: str, number_ids: int, max_int_time: int, start_idx: int,
+                          end_idx: int):
         """
         Finds a communication interval with respect to the given strategy. The interval is maximum of the given seconds 
         and has at least number_ids communicating initiators in it.
@@ -78,7 +86,8 @@ class CommunicationProcessor():
                 start_idx -= 1  # because message indices start with 1 (for the user)
                 interval = cpp_comm_proc.find_interval_from_startidx(start_idx, number_ids, max_int_time)
             elif start_idx and end_idx:
-                start_idx -= 1; end_idx -= 1
+                start_idx -= 1
+                end_idx -= 1
                 ids = cpp_comm_proc.get_interval_init_ids(start_idx, end_idx)
                 if not ids:
                     return {}
@@ -104,8 +113,9 @@ class CommunicationProcessor():
         msgs, msg_id = [], 0
         # keep track of previous request to find connections
         prev_reqs = {}
-        # used to determine whether a request has been seen yet, so that replies before the first request are skipped and do not throw an error by
-        # accessing the empty dict prev_reqs (this is not a perfect solution, but it works most of the time)
+        # used to determine whether a request has been seen yet, so that replies before the first request are skipped
+        # and do not throw an error by accessing the empty dict prev_reqs (this is not a perfect solution, but it works
+        # most of the time)
         req_seen = False
         local_init_ids = self.local_init_ids
         external_init_ids = set()
@@ -132,7 +142,7 @@ class CommunicationProcessor():
                     respnd_ids.add(id_dst)
                 # convert the abstract message into a message object to handle it better
                 msg_str = "{0}-{1}".format(id_src, id_dst)
-                msg = Message(msg_id, id_src, id_dst, msg_type, time, line_no = lineno)
+                msg = Message(msg_id, id_src, id_dst, msg_type, time, line_no=lineno)
                 msgs.append(msg)
                 prev_reqs[msg_str] = msg_id
                 msg_id += 1
@@ -188,7 +198,6 @@ class CommunicationProcessor():
         """
         Map the given IDs to a locality (i.e. local or external} considering the given probabilities.
 
-        :param comm_type: the type of communication (i.e. local, external or mixed)
         :param prob_rspnd_local: the probabilty that a responder is local
         """
         external_ids = set()
@@ -212,4 +221,4 @@ class CommunicationProcessor():
                 external_ids.add(id_)
 
         self.local_ids, self.external_ids = local_ids, external_ids
-        return self.local_ids, self.external_ids
+        return self.local_ids, self.external_ids

+ 51 - 47
code/ID2TLib/FileUtils.py

@@ -2,57 +2,61 @@ import xml.etree.ElementTree as ElementTree
 import csv
 import os
 
+
 def parse_xml(filepath: str):
-	'''
-	Parses an XML File
-	It is assumed, that packets are placed on the second hierarchical level and packetinformation is encoded as attributes
+    """
+    Parses an XML File
+    It is assumed, that packets are placed on the second hierarchical level and packet-information is encoded
+    as attributes
+
+    :param filepath: the path to the XML file to be parsed
+    :return: a List of Dictionaries, each Dictionary contains the information of one packet
+    """
 
-	:param filepath: the path to the XML file to be parsed
-	:return: a List of Dictionaries, each Dictionary contains the information of one packet
-	'''
+    tree = ElementTree.parse(filepath)
+    root = tree.getroot()
 
-	tree = ElementTree.parse(filepath)
-	root = tree.getroot()
+    # Convert Tree to List of Dictionaries
+    packets = []
+    for child in root:
+        packets.append(child.attrib)
 
-	#Convert Tree to List of Dictionaries
-	packets = []
-	for child in root:
-		packets.append(child.attrib)
+    return packets
 
-	return packets
 
 def parse_csv_to_xml(filepath: str):
-	'''
-	Converts a CSV file into an XML file. Every entry is converted to a child with respective attributes of the root node
-
-	:param filepath: the path to the CSV file to be parsed
-	:return: a path to the newly created XML file
-	'''
-
-	filename = os.path.splitext(filepath)[0]
-	# build a tree structure
-	root = ElementTree.Element("trace")
-	root.attrib["path"] = filename
-
-	# parse the csvFile into reader
-	with open(filepath, "rt") as csvFile:
-		reader = csv.reader(csvFile, delimiter=",")
-		# loop through the parsed file, creating packet-elements with the structure of the csvFile as attributes
-		lineno = -1 # lines start at zero
-		for line in reader:
-			lineno += 1
-			if not line:
-				continue
-
-			packet = ElementTree.SubElement(root, "packet")
-			for element in line:
-				element = element.replace(" ", "")
-				key, value = element.split(":")
-				packet.attrib[key] = str(value)
-			packet.attrib["LineNumber"] = str(lineno)
-
-	# writing the ElementTree into the .xml file
-	tree = ElementTree.ElementTree(root)
-	filepath = filename + ".xml"
-	tree.write(filepath)
-	return filepath
+    """
+    Converts a CSV file into an XML file. Every entry is converted to a child with respective attributes of the
+    root node
+
+    :param filepath: the path to the CSV file to be parsed
+    :return: a path to the newly created XML file
+    """
+
+    filename = os.path.splitext(filepath)[0]
+    # build a tree structure
+    root = ElementTree.Element("trace")
+    root.attrib["path"] = filename
+
+    # parse the csvFile into reader
+    with open(filepath, "rt") as csvFile:
+        reader = csv.reader(csvFile, delimiter=",")
+        # loop through the parsed file, creating packet-elements with the structure of the csvFile as attributes
+        lineno = -1  # lines start at zero
+        for line in reader:
+            lineno += 1
+            if not line:
+                continue
+
+            packet = ElementTree.SubElement(root, "packet")
+            for element in line:
+                element = element.replace(" ", "")
+                key, value = element.split(":")
+                packet.attrib[key] = str(value)
+            packet.attrib["LineNumber"] = str(lineno)
+
+    # writing the ElementTree into the .xml file
+    tree = ElementTree.ElementTree(root)
+    filepath = filename + ".xml"
+    tree.write(filepath)
+    return filepath

+ 265 - 260
code/ID2TLib/IPv4.py

@@ -1,269 +1,274 @@
 import re
+from typing import List, Union, Tuple, Optional, cast
 
 
 class IPAddress:
-	"""
-	A simple class encapsulating an ip-address. An IPAddress can be constructed by string, int and 4-element-list
-	(e.g. [8, 8, 8, 8]). This is a leightweight class as it only contains string-to-ip-and-reverse-conversion
-	and some convenience methods.
-	"""
-	
-	# a number between 0 and 255, no leading zeros
-	_IP_NUMBER_REGEXP = r"(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)"
-	# 4 numbers between 0 and 255, joined together with dots
-	IP_REGEXP = r"{0}\.{0}\.{0}\.{0}".format(_IP_NUMBER_REGEXP)
-	
-	def __init__(self, intlist: "list[int]") -> "IPAddress":
-		"""
-		Construct an ipv4-address with a list of 4 integers, e.g. to construct the ip 10.0.0.0 pass [10, 0, 0, 0]
-		"""
-		if not isinstance(intlist, list) or not all(isinstance(n, int) for n in intlist):
-			raise TypeError("The first constructor argument must be an list of ints")
-		if not len(intlist) == 4 or not all(0 <= n <= 255 for n in intlist):
-			raise ValueError("The integer list must contain 4 ints in range of 0 and 255, like an ip-address")
-		
-		# For easier calculations store the ip as integer, e.g. 10.0.0.0 is 0x0a000000
-		self.ipnum = int.from_bytes(bytes(intlist), "big")
-	
-	@staticmethod
-	def parse(ip: str) -> "IPAddress":
-		"""
-		Parse an ip-address-string. If the string does not comply to the ipv4-format a ValueError is raised
-		:param ip: A string-representation of an ip-address, e.g. "10.0.0.0"
-		:return: IPAddress-object describing the ip-address
-		"""
-		match = re.match("^" + IPAddress.IP_REGEXP + "$", ip)
-		if not match:
-			raise ValueError("%s is no ipv4-address" % ip)
-		
-		# the matches we get are the numbers of the ip-address (match 0 is the whole ip-address)
-		numbers = [int(match.group(i)) for i in range(1, 5)]
-		return IPAddress(numbers)
-	
-	@staticmethod
-	def from_int(numeric: int) -> "IPAddress":
-		if numeric not in range(1 << 32):
-			raise ValueError("numeric value must be in uint-range")
-		
-		# to_bytes is the easiest way to split a 32-bit int into bytes
-		return IPAddress(list(numeric.to_bytes(4, "big")))
-	
-	@staticmethod
-	def is_ipv4(ip: str) -> bool:
-		"""
-		Check if the supplied string is in ipv4-format
-		"""
-		
-		match = re.match("^" + IPAddress.IP_REGEXP + "$", ip)
-		return True if match else False
-
-	def to_int(self) -> int:
-		"""
-		Convert the ip-address to a 32-bit uint, e.g. IPAddress.parse("10.0.0.255").to_int() returns 0x0a0000ff
-		"""
-		return self.ipnum
-	
-	def is_private(self) -> bool:
-		"""
-		Returns a boolean indicating if the ip-address lies in the private ip-segments (see ReservedIPBlocks)
-		"""
-		return ReservedIPBlocks.is_private(self)
-	
-	def get_private_segment(self) -> bool:
-		"""
-		Return the private ip-segment the ip-address belongs to (there are several)
-		If this ip does not belong to a private ip-segment a ValueError is raised
-		:return: IPAddressBlock
-		"""
-		return ReservedIPBlocks.get_private_segment(self)
-
-	def is_localhost(self) -> bool:
-		"""
-		Returns a boolean indicating if the ip-address lies in the localhost-segment
-		"""
-		return ReservedIPBlocks.is_localhost(self)
-	
-	def is_multicast(self) -> bool:
-		"""
-		Returns a boolean indicating if the ip-address lies in the multicast-segment
-		"""
-		return ReservedIPBlocks.is_multicast(self)
-	
-	def is_reserved(self) -> bool:
-		"""
-		Returns a boolean indicating if the ip-address lies in the reserved-segment
-		"""
-		return ReservedIPBlocks.is_reserved(self)
-	
-	def is_zero_conf(self) -> bool:
-		"""
-		Returns a boolean indicating if the ip-address lies in the zeroconf-segment
-		"""
-		return ReservedIPBlocks.is_zero_conf(self)
-	
-	def _tuple(self) -> (int,int,int,int):
-		return tuple(self.ipnum.to_bytes(4, "big"))
-	
-	def __repr__(self) -> str:
-		"""
-		Following the python style guide, eval(repr(obj)) should equal obj
-		"""
-		return "IPAddress([%i, %i, %i, %i])" % self._tuple()
-	
-	def __str__(self) -> str:
-		"""
-		Return the ip-address described by this object in ipv4-format
-		"""
-		return "%i.%i.%i.%i" % self._tuple()
-	
-	def __hash__(self) -> int:
-		return self.ipnum
-	
-	def __eq__(self, other) -> bool:
-		if other is None:
-			return False
-		
-		return isinstance(other, IPAddress) and self.ipnum == other.ipnum
-	
-	def __lt__(self, other) -> bool:
-		if other is None:
-			raise TypeError("Cannot compare to None")
-		if not isinstance(other, IPAddress):
-			raise NotImplemented # maybe other can compare to self
-		
-		return self.ipnum < other.ipnum
-	
-	def __int__(self) -> bool:
-		return self.ipnum
+    """
+    A simple class encapsulating an ip-address. An IPAddress can be constructed by string, int and 4-element-list
+    (e.g. [8, 8, 8, 8]). This is a lightweight class as it only contains string-to-ip-and-reverse-conversion
+    and some convenience methods.
+    """
+
+    # a number between 0 and 255, no leading zeros
+    _IP_NUMBER_REGEXP = r"(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)"
+    # 4 numbers between 0 and 255, joined together with dots
+    IP_REGEXP = r"{0}\.{0}\.{0}\.{0}".format(_IP_NUMBER_REGEXP)
+
+    def __init__(self, intlist: List[int]) -> None:
+        """
+        Construct an ipv4-address with a list of 4 integers, e.g. to construct the ip 10.0.0.0 pass [10, 0, 0, 0]
+        """
+        if not isinstance(intlist, list) or not all(isinstance(n, int) for n in intlist):
+            raise TypeError("The first constructor argument must be an list of ints")
+        if not len(intlist) == 4 or not all(0 <= n <= 255 for n in intlist):
+            raise ValueError("The integer list must contain 4 ints in range of 0 and 255, like an ip-address")
+
+        # For easier calculations store the ip as integer, e.g. 10.0.0.0 is 0x0a000000
+        self.ipnum = int.from_bytes(bytes(intlist), "big")
+
+    @staticmethod
+    def parse(ip: str) -> "IPAddress":
+        """
+        Parse an ip-address-string. If the string does not comply to the ipv4-format a ValueError is raised
+        :param ip: A string-representation of an ip-address, e.g. "10.0.0.0"
+        :return: IPAddress-object describing the ip-address
+        """
+        match = re.match("^" + IPAddress.IP_REGEXP + "$", ip)
+        if not match:
+            raise ValueError("%s is no ipv4-address" % ip)
+
+        # the matches we get are the numbers of the ip-address (match 0 is the whole ip-address)
+        numbers = [int(match.group(i)) for i in range(1, 5)]
+        return IPAddress(numbers)
+
+    @staticmethod
+    def from_int(numeric: int) -> "IPAddress":
+        if numeric not in range(1 << 32):
+            raise ValueError("numeric value must be in uint-range")
+
+        # to_bytes is the easiest way to split a 32-bit int into bytes
+        return IPAddress(list(numeric.to_bytes(4, "big")))
+
+    @staticmethod
+    def is_ipv4(ip: str) -> bool:
+        """
+        Check if the supplied string is in ipv4-format
+        """
+
+        match = re.match("^" + IPAddress.IP_REGEXP + "$", ip)
+        return True if match else False
+
+    def to_int(self) -> int:
+        """
+        Convert the ip-address to a 32-bit uint, e.g. IPAddress.parse("10.0.0.255").to_int() returns 0x0a0000ff
+        """
+        return self.ipnum
+
+    def is_private(self) -> bool:
+        """
+        Returns a boolean indicating if the ip-address lies in the private ip-segments (see ReservedIPBlocks)
+        """
+        return ReservedIPBlocks.is_private(self)
+
+    def get_private_segment(self) -> "IPAddressBlock":
+        """
+        Return the private ip-segment the ip-address belongs to (there are several)
+        If this ip does not belong to a private ip-segment a ValueError is raised
+        :return: IPAddressBlock
+        """
+        return ReservedIPBlocks.get_private_segment(self)
+
+    def is_localhost(self) -> bool:
+        """
+        Returns a boolean indicating if the ip-address lies in the localhost-segment
+        """
+        return ReservedIPBlocks.is_localhost(self)
+
+    def is_multicast(self) -> bool:
+        """
+        Returns a boolean indicating if the ip-address lies in the multicast-segment
+        """
+        return ReservedIPBlocks.is_multicast(self)
+
+    def is_reserved(self) -> bool:
+        """
+        Returns a boolean indicating if the ip-address lies in the reserved-segment
+        """
+        return ReservedIPBlocks.is_reserved(self)
+
+    def is_zero_conf(self) -> bool:
+        """
+        Returns a boolean indicating if the ip-address lies in the zeroconf-segment
+        """
+        return ReservedIPBlocks.is_zero_conf(self)
+
+    def _tuple(self) -> Tuple[int, int, int, int]:
+        return cast(Tuple[int, int, int, int], tuple(self.ipnum.to_bytes(4, "big")))
+
+    def __repr__(self) -> str:
+        """
+        Following the python style guide, eval(repr(obj)) should equal obj
+        """
+        return "IPAddress([%i, %i, %i, %i])" % self._tuple()
+
+    def __str__(self) -> str:
+        """
+        Return the ip-address described by this object in ipv4-format
+        """
+        return "%i.%i.%i.%i" % self._tuple()
+
+    def __hash__(self) -> int:
+        return self.ipnum
+
+    def __eq__(self, other) -> bool:
+        if other is None:
+            return False
+
+        return isinstance(other, IPAddress) and self.ipnum == other.ipnum
+
+    def __lt__(self, other) -> bool:
+        if other is None:
+            raise TypeError("Cannot compare to None")
+        if not isinstance(other, IPAddress):
+            raise NotImplemented  # maybe other can compare to self
+
+        return self.ipnum < other.ipnum
+
+    def __int__(self) -> int:
+        return self.ipnum
+
 
 class IPAddressBlock:
-	"""
-	This class describes a block of IPv4-addresses, just as a string in CIDR-notation does.
-	It can be seen as a range of ip-addresses. To check if a block contains a ip-address
-	simply use "ip in ip_block"
-	"""
-	
-	# this regex describes CIDR-notation (an ip-address plus "/XX", whereas XX is a number between 1 and 32)
-	CIDR_REGEXP = IPAddress.IP_REGEXP + r"(\/(3[0-2]|[12]?\d)|)?"
-	
-	def __init__(self, ip: "Union(str, list, IPAddress)", netmask = 32) -> "IPAddressBlock":
-		"""
-		Construct a ip-block given a ip-address and a netmask. Given an ip and a netmask,
-		the constructed ip-block will describe the range ip/netmask (e.g. 127.0.0.1/8)
-		:param ip: An ip-address, represented as IPAddress, string or 4-element-list
-		"""
-		if isinstance(ip, str):
-			ip = IPAddress.parse(ip)
-		elif isinstance(ip, list):
-			ip = IPAddress(ip)
-		
-		if not 1 <= netmask <= 32:
-			raise ValueError("netmask must lie between 1 and 32")
-		
-		# clear the unnecessary bits in the base-ip, e.g. this will convert 10.0.0.255/24 to 10.0.0.0/24 which are equivalent
-		self.ipnum = ip.to_int() & self._bitmask(netmask)
-		self.netmask = netmask
-
-	@staticmethod
-	def parse(cidr: str) -> "IPAddressBlock":
-		"""
-		Parse a string in cidr-notation and return a IPAddressBlock describing the ip-segment
-		If the string is not in cidr-notation a ValueError is raised
-		"""
-		
-		match = re.match("^" + IPAddressBlock.CIDR_REGEXP + "$", cidr)
-		if not match:
-			raise ValueError("%s is no valid cidr-notation" % cidr)
-		
-		ip = [int(match.group(i)) for i in range(1, 5)]
-		suffix = 32 if not match.group(6) else int(match.group(6))
-		
-		return IPAddressBlock(ip, suffix)
-	
-	def block_size(self) -> int:
-		"""
-		Return the size of the ip-address-block. E.g. the size of someip/24 is 256
-		"""
-		return 2 ** (32 - self.netmask)
-	
-	def first_address(self) -> IPAddress:
-		"""
-		Return the first ip-address of the ip-block
-		"""
-		return IPAddress.from_int(self.ipnum)
-
-	def last_address(self) -> IPAddress:
-		"""
-		Return the last ip-address of the ip-block
-		"""
-		return IPAddress.from_int(self.ipnum + self.block_size() - 1)
-
-	def _bitmask(self, netmask: int) -> int:
-		ones = lambda x: (1 << x) - 1
-		
-		return ones(32) ^ ones(32 - netmask)
-	
-	def __repr__(self) -> str:
-		"""
-		Conforming to python style-guide, eval(repr(obj)) equals obj
-		"""
-		return "IPAddressBlock(%s, %i)" % (repr(IPAddress.from_int(self.ipnum)), self.netmask)
-	
-	def __str__(self) -> str:
-		"""
-		Return a string in cidr-notation
-		"""
-		return str(IPAddress.from_int(self.ipnum)) + "/" + str(self.netmask)
-	
-	def __contains__(self, ip: IPAddress) -> bool:
-		return (ip.to_int() & self._bitmask(self.netmask)) == self.ipnum
+    """
+    This class describes a block of IPv4-addresses, just as a string in CIDR-notation does.
+    It can be seen as a range of ip-addresses. To check if a block contains a ip-address
+    simply use "ip in ip_block"
+    """
+
+    # this regex describes CIDR-notation (an ip-address plus "/XX", whereas XX is a number between 1 and 32)
+    CIDR_REGEXP = IPAddress.IP_REGEXP + r"(\/(3[0-2]|[12]?\d)|)?"
+
+    def __init__(self, ip: Union[str, List[int], IPAddress], netmask: int=32) -> None:
+        """
+        Construct a ip-block given a ip-address and a netmask. Given an ip and a netmask,
+        the constructed ip-block will describe the range ip/netmask (e.g. 127.0.0.1/8)
+        :param ip: An ip-address, represented as IPAddress, string or 4-element-list
+        """
+        if isinstance(ip, str):
+            ip = IPAddress.parse(ip)
+        elif isinstance(ip, list):
+            ip = IPAddress(ip)
+
+        if not 1 <= netmask <= 32:
+            raise ValueError("netmask must lie between 1 and 32")
+
+        # clear the unnecessary bits in the base-ip, e.g. this will convert 10.0.0.255/24 to 10.0.0.0/24 which are equivalent
+        self.ipnum = ip.to_int() & self._bitmask(netmask)
+        self.netmask = netmask
+
+    @staticmethod
+    def parse(cidr: str) -> "IPAddressBlock":
+        """
+        Parse a string in cidr-notation and return a IPAddressBlock describing the ip-segment
+        If the string is not in cidr-notation a ValueError is raised
+        """
+
+        match = re.match("^" + IPAddressBlock.CIDR_REGEXP + "$", cidr)
+        if not match:
+            raise ValueError("%s is no valid cidr-notation" % cidr)
+
+        ip = [int(match.group(i)) for i in range(1, 5)]
+        suffix = 32 if not match.group(6) else int(match.group(6))
+
+        return IPAddressBlock(ip, suffix)
+
+    def block_size(self) -> int:
+        """
+        Return the size of the ip-address-block. E.g. the size of someip/24 is 256
+        """
+        return 2 ** (32 - self.netmask)
+
+    def first_address(self) -> IPAddress:
+        """
+        Return the first ip-address of the ip-block
+        """
+        return IPAddress.from_int(self.ipnum)
+
+    def last_address(self) -> IPAddress:
+        """
+        Return the last ip-address of the ip-block
+        """
+        return IPAddress.from_int(self.ipnum + self.block_size() - 1)
+
+    def _bitmask(self, netmask: int) -> int:
+        def ones(x: int) -> int:
+            return (1 << x) - 1
+
+        return ones(32) ^ ones(32 - netmask)
+
+    def __repr__(self) -> str:
+        """
+        Conforming to python style-guide, eval(repr(obj)) equals obj
+        """
+        return "IPAddressBlock(%s, %i)" % (repr(IPAddress.from_int(self.ipnum)), self.netmask)
+
+    def __str__(self) -> str:
+        """
+        Return a string in cidr-notation
+        """
+        return str(IPAddress.from_int(self.ipnum)) + "/" + str(self.netmask)
+
+    def __contains__(self, ip: IPAddress) -> bool:
+        return (ip.to_int() & self._bitmask(self.netmask)) == self.ipnum
+
 
 class ReservedIPBlocks:
-	"""
-	To avoid magic values and save developers some research this class contains several constants
-	describing special network-segments and some is_-methods to check if an ip is in the specified segment.
-	"""
-	
- 	# a list of ip-addresses that can be used in private networks
-	PRIVATE_IP_SEGMENTS = [
-		IPAddressBlock.parse(block)
-		for block in
-		("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")
-	]
-	
-	LOCALHOST_SEGMENT = IPAddressBlock.parse("127.0.0.0/8")
-	
-	MULTICAST_SEGMENT = IPAddressBlock.parse("224.0.0.0/4")
-	RESERVED_SEGMENT = IPAddressBlock.parse("240.0.0.0/4")
-	
-	ZERO_CONF_SEGMENT = IPAddressBlock.parse("169.254.0.0/16")
-	
-	@staticmethod
-	def is_private(ip: IPAddress) -> bool:
-		return any(ip in block for block in ReservedIPBlocks.PRIVATE_IP_SEGMENTS)
-	
-	@staticmethod
-	def get_private_segment(ip: IPAddress) -> "Optional[IPAddressBlock]":
-		if not ReservedIPBlocks.is_private(ip):
-			raise ValueError("%s is not part of a private IP segment" % ip)
-
-		for block in ReservedIPBlocks.PRIVATE_IP_SEGMENTS:
-			if ip in block:
-				return block
-
-	@staticmethod
-	def is_localhost(ip: IPAddress) -> bool:
-		return ip in ReservedIPBlocks.LOCALHOST_SEGMENT
-	
-	@staticmethod
-	def is_multicast(ip: IPAddressBlock) -> bool:
-		return ip in ReservedIPBlocks.MULTICAST_SEGMENT
-	
-	@staticmethod
-	def is_reserved(ip: IPAddress) -> bool:
-		return ip in ReservedIPBlocks.RESERVED_SEGMENT
-	
-	@staticmethod
-	def is_zero_conf(ip: IPAddressBlock) -> bool:
-		return ip in ReservedIPBlocks.ZERO_CONF_SEGMENT
+    """
+    To avoid magic values and save developers some research this class contains several constants
+    describing special network-segments and some is_-methods to check if an ip is in the specified segment.
+    """
+
+    # a list of ip-addresses that can be used in private networks
+    PRIVATE_IP_SEGMENTS = [
+        IPAddressBlock.parse(block)
+        for block in
+        ("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")
+    ]
+
+    LOCALHOST_SEGMENT = IPAddressBlock.parse("127.0.0.0/8")
+
+    MULTICAST_SEGMENT = IPAddressBlock.parse("224.0.0.0/4")
+    RESERVED_SEGMENT = IPAddressBlock.parse("240.0.0.0/4")
+
+    ZERO_CONF_SEGMENT = IPAddressBlock.parse("169.254.0.0/16")
+
+    @staticmethod
+    def is_private(ip: IPAddress) -> bool:
+        return any(ip in block for block in ReservedIPBlocks.PRIVATE_IP_SEGMENTS)
+
+    @staticmethod
+    def get_private_segment(ip: IPAddress) -> Optional[IPAddressBlock]:
+        if not ReservedIPBlocks.is_private(ip):
+            raise ValueError("%s is not part of a private IP segment" % ip)
+
+        for block in ReservedIPBlocks.PRIVATE_IP_SEGMENTS:
+            if ip in block:
+                return block
+
+        return None
+
+    @staticmethod
+    def is_localhost(ip: IPAddress) -> bool:
+        return ip in ReservedIPBlocks.LOCALHOST_SEGMENT
+
+    @staticmethod
+    def is_multicast(ip: IPAddress) -> bool:
+        return ip in ReservedIPBlocks.MULTICAST_SEGMENT
+
+    @staticmethod
+    def is_reserved(ip: IPAddress) -> bool:
+        return ip in ReservedIPBlocks.RESERVED_SEGMENT
 
+    @staticmethod
+    def is_zero_conf(ip: IPAddress) -> bool:
+        return ip in ReservedIPBlocks.ZERO_CONF_SEGMENT

+ 1 - 1
code/ID2TLib/Label.py

@@ -21,7 +21,7 @@ class Label:
         self.parameters = parameters
 
     def __eq__(self, other):
-        return self.timestamp == other.timestamp
+        return self.timestamp_start == other.timestamp_start
 
     def __lt__(self, other):
         return self.timestamp_start < other.timestamp_start

+ 248 - 239
code/ID2TLib/Ports.py

@@ -1,251 +1,260 @@
-import random, copy
+import random
+import copy
+
 
 # information taken from https://www.cymru.com/jtk/misc/ephemeralports.html
 class PortRanges:
-	# dynamic ports as listed by RFC 6056
-	DYNAMIC_PORTS = range(49152, 65536)
-	
-	LINUX = range(32768, 61001)
-	FREEBSD = range(10000, 65536)
-	
-	APPLE_IOS = DYNAMIC_PORTS
-	APPLE_OSX = DYNAMIC_PORTS
-	
-	WINDOWS_7 = DYNAMIC_PORTS
-	WINDOWS_8 = DYNAMIC_PORTS
-	WINDOWS_VISTA = DYNAMIC_PORTS
-	WINDOWS_XP = range(1024, 5001)
+    # dynamic ports as listed by RFC 6056
+    DYNAMIC_PORTS = range(49152, 65536)
+
+    LINUX = range(32768, 61001)
+    FREEBSD = range(10000, 65536)
+
+    APPLE_IOS = DYNAMIC_PORTS
+    APPLE_OSX = DYNAMIC_PORTS
+
+    WINDOWS_7 = DYNAMIC_PORTS
+    WINDOWS_8 = DYNAMIC_PORTS
+    WINDOWS_VISTA = DYNAMIC_PORTS
+    WINDOWS_XP = range(1024, 5001)
+
 
 # This class uses classes instead of functions so deepcloning works
 class PortSelectionStrategy:
-	class sequential:
-		def __init__(self):
-			self.counter = -1
-		
-		# that function will always return a one higher counter than before,
-		# restarting from the start once it reached the highest value
-		def __call__(self, port_range, *args):
-			if self.counter == -1:
-				self.counter = port_range.start
-			
-			port = self.counter
-			
-			self.counter += 1
-			if self.counter == port_range.stop:
-				self.counter = port_range.start
-			
-			return port
-	class random:
-		def __call__(self, port_range, *args):
-			return random.randrange(port_range.start, port_range.stop)
-
-	class linux_kernel:
-		"""
-		A port-selectioin-strategy oriented on the linux-kernel
-		The implementation follows https://github.com/torvalds/linux/blob/master/net/ipv4/inet_connection_sock.c#L173
-		as much as possible when converting from one language to another (The newest file was used
-		by the time of writing, make sure you select the correct one when following the link!)
-		"""
-
-		def __call__(self, port_range: range, port_selector, *args):
-			"""
-			This method is an attempt to map a c-function to python. To solve the goto-problem
-			while-true's have been added. Both of the while-true's are placed where the original
-			had a label to jump to. break's and continue's are set to preserve the original
-			control flow. Another method could have been used to rewrite the c-code, however this
-			was chosen to preserve the similarity between this and the original
-
-			:param port_range: the port range to choose from
-			:param port_selector: the port selector that tells which ports are in use
-			:param args: Not used for now
-			:return: A port number
-			"""
-			port = 0
-			low, high = port_range.start, port_range.stop
-
-			# this var tells us if we should use the upper or lower port-range-half, or the whole range if
-			# this var is None. The original was an enum of the values 0, 1 and 2. But I think an Optional[bool]
-			# is more clear
-			# None: use whole range, True: use lower half, False: use upper half
-			attempt_half = True
-
-			high += 1  # line 186 in the original file
-			while True:
-				if high - low < 4:
-					attempt_half = None
-				if attempt_half is not None:
-					# appearently a fast method to find a number close to the real half
-					# unless the difference between high and low is 4 (see above, note the 2-shift below)
-					# this does not work
-					half = low + (((high - low) >> 2) << 1)
-
-					if attempt_half:
-						high = half
-					else:
-						low = half
-
-				remaining = high - low
-				if remaining > 1:
-					remaining &= ~1 # flip the 1-bit
-
-				offset = random.randrange(0, remaining)
-				offset |= 1;
-
-				attempt_half_before = attempt_half # slight hack to keep track of change
-				while True:
-					port = low + offset
-
-					for i in range(0, remaining, 2):
-						if port >= high:
-							port -= remaining
-
-						if port_selector.is_port_in_use(port):
-							port += 2
-							continue
-
-						return port
-
-					offset -= 1
-					if not (offset & 1):
-						continue
-
-					if attempt_half:
-						attempt_half = False
-						break
-
-				if attempt_half_before: # we still got ports to search, attemp_half was just set to False
-					continue
-				if not attempt_half: # the port-range is exhausted
-					break
-
-			raise ValueError("Could not find suitable port")
+    class sequential:
+        def __init__(self):
+            self.counter = -1
+
+        # that function will always return a one higher counter than before,
+        # restarting from the start once it reached the highest value
+        def __call__(self, port_range, *args):
+            if self.counter == -1:
+                self.counter = port_range.start
+
+            port = self.counter
+
+            self.counter += 1
+            if self.counter == port_range.stop:
+                self.counter = port_range.start
+
+            return port
+
+    class random:
+        def __call__(self, port_range, *args):
+            return random.randrange(port_range.start, port_range.stop)
+
+    class linux_kernel:
+        """
+        A port-selection-strategy oriented on the linux-kernel
+        The implementation follows https://github.com/torvalds/linux/blob/master/net/ipv4/inet_connection_sock.c#L173
+        as much as possible when converting from one language to another (The newest file was used
+        by the time of writing, make sure you select the correct one when following the link!)
+        """
+
+        def __call__(self, port_range: range, port_selector, *args):
+            """
+            This method is an attempt to map a c-function to python. To solve the goto-problem
+            while-true's have been added. Both of the while-true's are placed where the original
+            had a label to jump to. break's and continue's are set to preserve the original
+            control flow. Another method could have been used to rewrite the c-code, however this
+            was chosen to preserve the similarity between this and the original
+
+            :param port_range: the port range to choose from
+            :param port_selector: the port selector that tells which ports are in use
+            :param args: Not used for now
+            :return: A port number
+            """
+            low, high = port_range.start, port_range.stop
+
+            # this var tells us if we should use the upper or lower port-range-half, or the whole range if
+            # this var is None. The original was an enum of the values 0, 1 and 2. But I think an Optional[bool]
+            # is more clear
+            # None: use whole range, True: use lower half, False: use upper half
+            attempt_half = True
+
+            high += 1  # line 186 in the original file
+            while True:
+                if high - low < 4:
+                    attempt_half = None
+                if attempt_half is not None:
+                    # apparently a fast method to find a number close to the real half
+                    # unless the difference between high and low is 4 (see above, note the 2-shift below)
+                    # this does not work
+                    half = low + (((high - low) >> 2) << 1)
+
+                    if attempt_half:
+                        high = half
+                    else:
+                        low = half
+
+                remaining = high - low
+                if remaining > 1:
+                    remaining &= ~1  # flip the 1-bit
+
+                offset = random.randrange(0, remaining)
+                offset |= 1
+
+                attempt_half_before = attempt_half  # slight hack to keep track of change
+                while True:
+                    port = low + offset
+
+                    for i in range(0, remaining, 2):
+                        if port >= high:
+                            port -= remaining
+
+                        if port_selector.is_port_in_use(port):
+                            port += 2
+                            continue
+
+                        return port
+
+                    offset -= 1
+                    if not (offset & 1):
+                        continue
+
+                    if attempt_half:
+                        attempt_half = False
+                        break
+
+                if attempt_half_before:  # we still got ports to search, attemp_half was just set to False
+                    continue
+                if not attempt_half:  # the port-range is exhausted
+                    break
+
+            raise ValueError("Could not find suitable port")
+
 
 class PortSelector:
-	"""
-	This class simulates a port-selection-process. Instances keep a list of port-numbers they generated so
-	the same port-number will not be generated again.
-	"""
-	
-	def __init__(self, port_range, select_function):
-		"""
-		Create a PortSelector given a range of ports to choose from and a function that chooses the next port
-		
-		:param port_range: a range-object containing the range of ports to choose from
-		:param select_function: a function that receives the port_range and selects a port
-		"""
-		
-		if len(port_range) == 0:
-			raise ValueError("cannot choose from an empty range")
-		if port_range.start not in range(1, 65536) or port_range.stop not in range(1, 65536 + 1):
-			raise ValueError("port_range is no subset of the valid port-range")
-		
-		self.port_range = port_range
-		
-		self._select_port = select_function
-		
-		self.generated = []
-	
-	def select_port(self):
-		# do this check to avoid endless loops
-		if len(self.generated) == len(self.port_range):
-			raise RuntimeError("All %i port numbers were already generated, no more can be generated" % len(self.port_range))
-		
-		while True:
-			port = self._select_port(self.port_range, self)
-			
-			if port not in self.generated:
-				self.generated.append(port)
-				return port
-	
-	def is_port_in_use(self, port: int):
-		return port in self.generated
-	
-	def undo_port_use(self, port: int):
-		if port in self.generated:
-			self.generated.remove(port)
-		else:
-			raise ValueError("Port %i is not in use and thus can not be undone" % port)
-	
-	def reduce_size(self, size: int):
-		"""
-		Reduce the list of already generated ports to the last <size> generated.
-		If size if bigger than the number of generated ports nothing happens.
-		"""
-		self.generated = self.generated[-size:]
-	
-	def clear(self):
-		"""
-		Clear the list of generated ports. As of now this does not reset the state of the selection-function
-		"""
-		self.generated = []
-	
-	def clone(self):
-		return copy.deepcopy(self)
+    """
+    This class simulates a port-selection-process. Instances keep a list of port-numbers they generated so
+    the same port-number will not be generated again.
+    """
+
+    def __init__(self, port_range, select_function):
+        """
+        Create a PortSelector given a range of ports to choose from and a function that chooses the next port
+
+        :param port_range: a range-object containing the range of ports to choose from
+        :param select_function: a function that receives the port_range and selects a port
+        """
+
+        if len(port_range) == 0:
+            raise ValueError("cannot choose from an empty range")
+        if port_range.start not in range(1, 65536) or port_range.stop not in range(1, 65536 + 1):
+            raise ValueError("port_range is no subset of the valid port-range")
+
+        self.port_range = port_range
+
+        self._select_port = select_function
+
+        self.generated = []
+
+    def select_port(self):
+        # do this check to avoid endless loops
+        if len(self.generated) == len(self.port_range):
+            raise RuntimeError("All %i port numbers were already generated, no more can be generated"
+                               % len(self.port_range))
+
+        while True:
+            port = self._select_port(self.port_range, self)
+
+            if port not in self.generated:
+                self.generated.append(port)
+                return port
+
+    def is_port_in_use(self, port: int):
+        return port in self.generated
+
+    def undo_port_use(self, port: int):
+        if port in self.generated:
+            self.generated.remove(port)
+        else:
+            raise ValueError("Port %i is not in use and thus can not be undone" % port)
+
+    def reduce_size(self, size: int):
+        """
+        Reduce the list of already generated ports to the last <size> generated.
+        If size if bigger than the number of generated ports nothing happens.
+        """
+        self.generated = self.generated[-size:]
+
+    def clear(self):
+        """
+        Clear the list of generated ports. As of now this does not reset the state of the selection-function
+        """
+        self.generated = []
+
+    def clone(self):
+        return copy.deepcopy(self)
+
 
 class ProtocolPortSelector:
-	"""
-	This class contains a method to select ports for udp and tcp. It generally consists of the port-selectors, one
-	for tcp and one for udp. For convenience this class has a __getattr__-method to call methods on both selectors
-	at once. E.g, clear() does not exist for ProtocolPortSelector but it does for PortSelector, therefore
-	protocolPortSelector.clear() will call clear for both port-selectors.
-	"""
-	
-	def __init__(self, port_range, select_tcp, select_udp = None):
-		self.tcp = PortSelector(port_range, select_tcp)
-		self.udp = PortSelector(port_range, select_udp or select_tcp)
-	
-	def get_tcp_generator(self):
-		return self.tcp
-	
-	def get_udp_generator(self):
-		return self.udp
-	
-	def select_port_tcp(self):
-		return self.tcp.select_port()
-	
-	def select_port_udp(self):
-		return self.udp.select_port()
-	
-	def is_port_in_use_tcp(self, port):
-		return self.tcp.is_port_in_use(port)
-	
-	def is_port_in_use_udp(self, port):
-		return self.udp.is_port_in_use(port)
-	
-	def clone(self):
-		class Tmp: pass
-		clone = Tmp()
-		clone.__class__ = type(self)
-		
-		clone.udp = self.udp.clone()
-		clone.tcp = self.tcp.clone()
-		
-		return clone
-	
-	def __getattr__(self, attr):
-		val = getattr(self.tcp, attr)
-		
-		if callable(val): # we proprably got a method here
-			tcp_meth = val
-			udp_meth = getattr(self.udp, attr)
-			
-			def double_method(*args, **kwargs):
-				return (tcp_meth(*args, **kwargs), udp_meth(*args, **kwargs))
-			
-			return double_method # calling this function will call the method for both port-selectors
-		else: # we have found a simple value, return a tuple containing the attribute-value from both port-selectors
-			return (val, getattr(self.udp, attr))
+    """
+    This class contains a method to select ports for udp and tcp. It generally consists of the port-selectors, one
+    for tcp and one for udp. For convenience this class has a __getattr__-method to call methods on both selectors
+    at once. E.g, clear() does not exist for ProtocolPortSelector but it does for PortSelector, therefore
+    protocolPortSelector.clear() will call clear for both port-selectors.
+    """
 
-class PortSelectors:
-	"""
-	To save some time this class contains some of the port-selection-strategies found in the wild. It is recommend to use
-	.clone() to get your personal copy, otherwise two parts of your code might select ports on the same port-selector which
-	is something your might want to avoid.
-	"""
-	LINUX = ProtocolPortSelector(PortRanges.LINUX, PortSelectionStrategy.random())
-	APPLE = ProtocolPortSelector(PortRanges.DYNAMIC_PORTS,
-			PortSelectionStrategy.sequential(),
-			PortSelectionStrategy.random())
-	FREEBSD = ProtocolPortSelector(PortRanges.FREEBSD, PortSelectionStrategy.random())
-	WINDOWS = ProtocolPortSelector(PortRanges.WINDOWS_7, PortSelectionStrategy.random()) # the selection strategy is a guess as i can't find more info on it
+    def __init__(self, port_range, select_tcp, select_udp=None):
+        self.tcp = PortSelector(port_range, select_tcp)
+        self.udp = PortSelector(port_range, select_udp or select_tcp)
+
+    def get_tcp_generator(self):
+        return self.tcp
+
+    def get_udp_generator(self):
+        return self.udp
 
+    def select_port_tcp(self):
+        return self.tcp.select_port()
+
+    def select_port_udp(self):
+        return self.udp.select_port()
+
+    def is_port_in_use_tcp(self, port):
+        return self.tcp.is_port_in_use(port)
+
+    def is_port_in_use_udp(self, port):
+        return self.udp.is_port_in_use(port)
+
+    def clone(self):
+        class Tmp:
+            pass
+
+        clone = Tmp()
+        clone.__class__ = type(self)
+
+        clone.udp = self.udp.clone()
+        clone.tcp = self.tcp.clone()
+
+        return clone
+
+    def __getattr__(self, attr):
+        val = getattr(self.tcp, attr)
+
+        if callable(val):  # we proprably got a method here
+            tcp_meth = val
+            udp_meth = getattr(self.udp, attr)
+
+            def double_method(*args, **kwargs):
+                return tcp_meth(*args, **kwargs), udp_meth(*args, **kwargs)
+
+            return double_method  # calling this function will call the method for both port-selectors
+        else:  # we have found a simple value, return a tuple containing the attribute-value from both port-selectors
+            return val, getattr(self.udp, attr)
+
+
+class PortSelectors:
+    """
+    To save some time this class contains some of the port-selection-strategies found in the wild. It is recommended
+    to use .clone() to get your personal copy, otherwise two parts of your code might select ports on the same
+    port-selector which is something you might want to avoid.
+    """
+    LINUX = ProtocolPortSelector(PortRanges.LINUX, PortSelectionStrategy.random())
+    APPLE = ProtocolPortSelector(PortRanges.DYNAMIC_PORTS,
+                                 PortSelectionStrategy.sequential(),
+                                 PortSelectionStrategy.random())
+    FREEBSD = ProtocolPortSelector(PortRanges.FREEBSD, PortSelectionStrategy.random())
+    # the selection strategy is a guess as i can't find more info on it
+    WINDOWS = ProtocolPortSelector(PortRanges.WINDOWS_7, PortSelectionStrategy.random())

+ 4 - 2
code/Test/efficiency_testing.py

@@ -1,3 +1,4 @@
+import unittest as ut
 import unittest.mock as mock
 
 import ID2TLib.TestLibrary as Lib
@@ -50,5 +51,6 @@ class EfficiencyTests(Test.ID2TAttackTest):
         self.temporal_efficiency_test([['MS17Scan', 'ip.src=192.168.178.1']], time_limit=1.5, factor=1000)
 
     # FIXME: improve EternalBlue efficiency
-    #def test_EternalBlue(self):
-    #    self.temporal_efficiency_test([['EternalBlue']], time_limit=1.5, factor=1000)
+    @ut.skip("EternalBlue needs performance improvements to pass the efficiency test")
+    def test_EternalBlue(self):
+        self.temporal_efficiency_test([['EternalBlue']], time_limit=1.5, factor=1000)

+ 39 - 38
code/Test/test_ipgenerator.py

@@ -1,43 +1,44 @@
 from ID2TLib.Generator import IPGenerator
 import unittest
 
+
+# FIXME: These tests rely on randomness. They are NOT repeatable, and therefore unsuitable as unittests!
 class IPGeneratorTestCase(unittest.TestCase):
-	IP_GENERATOR = None
-	IP_SAMPLES = None
-	IP_SAMPLES_NUM = 1000
-	
-	@classmethod
-	def setUpClass(cls):
-		cls.IP_GENERATOR = IPGenerator()
-		cls.IP_SAMPLES = [cls.IP_GENERATOR.random_ip() for _ in range(cls.IP_SAMPLES_NUM)]
-	
-	def test_valid_ips(self):
-		ip = None
-		
-		try:
-			for ip in self.IP_SAMPLES:
-				parts = ip.split(".")
-				self.assertTrue(len(parts) == 4)
-				
-				numbers = [int(i) for i in parts]
-				self.assertTrue(all(n in range(256) for n in numbers))
-		except:
-			self.fail("%s is not a valid IPv4" % ip)
-	
-	def test_generates_localhost_ip(self):
-		self.assertFalse(any(ip.startswith("127.") for ip in self.IP_SAMPLES))
-	
-	def test_generates_private_ip(self):
-		def private_ip(ip):
-			private_starts = ["10.", "192.168."] + ["172.%i." % i for i in range(16, 32)]
-			return any(ip.startswith(start) for start in private_starts)
-		
-		self.assertFalse(any(map(private_ip, self.IP_SAMPLES)))
-	
-	def test_unique_ips(self):
-		self.assertTrue(len(self.IP_SAMPLES) == len(set(self.IP_SAMPLES)))
-	
-	def test_blacklist(self):
-		generator = IPGenerator(blacklist = ["42.0.0.0/8"])
-		self.assertFalse(any(generator.random_ip().startswith("42.") for _ in range(self.IP_SAMPLES_NUM)))
+    IP_GENERATOR = None
+    IP_SAMPLES = None
+    IP_SAMPLES_NUM = 1000
+
+    @classmethod
+    def setUpClass(cls):
+        cls.IP_GENERATOR = IPGenerator()
+        cls.IP_SAMPLES = [cls.IP_GENERATOR.random_ip() for _ in range(cls.IP_SAMPLES_NUM)]
+
+    def test_valid_ips(self):
+        ip = None
+
+        try:
+            for ip in self.IP_SAMPLES:
+                parts = ip.split(".")
+                self.assertTrue(len(parts) == 4)
+
+                numbers = [int(i) for i in parts]
+                self.assertTrue(all(n in range(256) for n in numbers))
+        except:
+            self.fail("%s is not a valid IPv4" % ip)
+
+    def test_generates_localhost_ip(self):
+        self.assertFalse(any(ip.startswith("127.") for ip in self.IP_SAMPLES))
+
+    def test_generates_private_ip(self):
+        def private_ip(ip):
+            private_starts = ["10.", "192.168."] + ["172.%i." % i for i in range(16, 32)]
+            return any(ip.startswith(start) for start in private_starts)
+
+        self.assertFalse(any(map(private_ip, self.IP_SAMPLES)))
+
+    def test_unique_ips(self):
+        self.assertTrue(len(self.IP_SAMPLES) == len(set(self.IP_SAMPLES)))
 
+    def test_blacklist(self):
+        generator = IPGenerator(blacklist=["42.0.0.0/8"])
+        self.assertFalse(any(generator.random_ip().startswith("42.") for _ in range(self.IP_SAMPLES_NUM)))