IPv4.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. import re
  2. from typing import List, Union, Tuple, Optional, cast
  3. class IPAddress:
  4. """
  5. A simple class encapsulating an ip-address. An IPAddress can be constructed by string, int and 4-element-list
  6. (e.g. [8, 8, 8, 8]). This is a lightweight class as it only contains string-to-ip-and-reverse-conversion
  7. and some convenience methods.
  8. """
  9. # a number between 0 and 255, no leading zeros
  10. _IP_NUMBER_REGEXP = r"(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)"
  11. # 4 numbers between 0 and 255, joined together with dots
  12. IP_REGEXP = r"{0}\.{0}\.{0}\.{0}".format(_IP_NUMBER_REGEXP)
  13. def __init__(self, intlist: List[int]) -> None:
  14. """
  15. 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]
  16. """
  17. if not isinstance(intlist, list) or not all(isinstance(n, int) for n in intlist):
  18. raise TypeError("The first constructor argument must be an list of ints")
  19. if not len(intlist) == 4 or not all(0 <= n <= 255 for n in intlist):
  20. raise ValueError("The integer list must contain 4 ints in range of 0 and 255, like an ip-address")
  21. # For easier calculations store the ip as integer, e.g. 10.0.0.0 is 0x0a000000
  22. self.ipnum = int.from_bytes(bytes(intlist), "big")
  23. @staticmethod
  24. def parse(ip: str) -> "IPAddress":
  25. """
  26. Parse an ip-address-string. If the string does not comply to the ipv4-format a ValueError is raised
  27. :param ip: A string-representation of an ip-address, e.g. "10.0.0.0"
  28. :return: IPAddress-object describing the ip-address
  29. """
  30. match = re.match("^" + IPAddress.IP_REGEXP + "$", ip)
  31. if not match:
  32. raise ValueError("%s is no ipv4-address" % ip)
  33. # the matches we get are the numbers of the ip-address (match 0 is the whole ip-address)
  34. numbers = [int(match.group(i)) for i in range(1, 5)]
  35. return IPAddress(numbers)
  36. @staticmethod
  37. def from_int(numeric: int) -> "IPAddress":
  38. if numeric not in range(1 << 32):
  39. raise ValueError("numeric value must be in uint-range")
  40. # to_bytes is the easiest way to split a 32-bit int into bytes
  41. return IPAddress(list(numeric.to_bytes(4, "big")))
  42. @staticmethod
  43. def is_ipv4(ip: str) -> bool:
  44. """
  45. Check if the supplied string is in ipv4-format
  46. """
  47. match = re.match("^" + IPAddress.IP_REGEXP + "$", ip)
  48. return True if match else False
  49. def to_int(self) -> int:
  50. """
  51. Convert the ip-address to a 32-bit uint, e.g. IPAddress.parse("10.0.0.255").to_int() returns 0x0a0000ff
  52. """
  53. return self.ipnum
  54. def is_private(self) -> bool:
  55. """
  56. Returns a boolean indicating if the ip-address lies in the private ip-segments (see ReservedIPBlocks)
  57. """
  58. return ReservedIPBlocks.is_private(self)
  59. def get_private_segment(self) -> "IPAddressBlock":
  60. """
  61. Return the private ip-segment the ip-address belongs to (there are several)
  62. If this ip does not belong to a private ip-segment a ValueError is raised
  63. :return: IPAddressBlock
  64. """
  65. return ReservedIPBlocks.get_private_segment(self)
  66. def is_localhost(self) -> bool:
  67. """
  68. Returns a boolean indicating if the ip-address lies in the localhost-segment
  69. """
  70. return ReservedIPBlocks.is_localhost(self)
  71. def is_multicast(self) -> bool:
  72. """
  73. Returns a boolean indicating if the ip-address lies in the multicast-segment
  74. """
  75. return ReservedIPBlocks.is_multicast(self)
  76. def is_reserved(self) -> bool:
  77. """
  78. Returns a boolean indicating if the ip-address lies in the reserved-segment
  79. """
  80. return ReservedIPBlocks.is_reserved(self)
  81. def is_zero_conf(self) -> bool:
  82. """
  83. Returns a boolean indicating if the ip-address lies in the zeroconf-segment
  84. """
  85. return ReservedIPBlocks.is_zero_conf(self)
  86. def _tuple(self) -> Tuple[int, int, int, int]:
  87. return cast(Tuple[int, int, int, int], tuple(self.ipnum.to_bytes(4, "big")))
  88. def __repr__(self) -> str:
  89. """
  90. Following the python style guide, eval(repr(obj)) should equal obj
  91. """
  92. return "IPAddress([%i, %i, %i, %i])" % self._tuple()
  93. def __str__(self) -> str:
  94. """
  95. Return the ip-address described by this object in ipv4-format
  96. """
  97. return "%i.%i.%i.%i" % self._tuple()
  98. def __hash__(self) -> int:
  99. return self.ipnum
  100. def __eq__(self, other) -> bool:
  101. if other is None:
  102. return False
  103. return isinstance(other, IPAddress) and self.ipnum == other.ipnum
  104. def __lt__(self, other) -> bool:
  105. if other is None:
  106. raise TypeError("Cannot compare to None")
  107. if not isinstance(other, IPAddress):
  108. raise NotImplemented # maybe other can compare to self
  109. return self.ipnum < other.ipnum
  110. def __int__(self) -> int:
  111. return self.ipnum
  112. class IPAddressBlock:
  113. """
  114. This class describes a block of IPv4-addresses, just as a string in CIDR-notation does.
  115. It can be seen as a range of ip-addresses. To check if a block contains a ip-address
  116. simply use "ip in ip_block"
  117. """
  118. # this regex describes CIDR-notation (an ip-address plus "/XX", whereas XX is a number between 1 and 32)
  119. CIDR_REGEXP = IPAddress.IP_REGEXP + r"(\/(3[0-2]|[12]?\d)|)?"
  120. def __init__(self, ip: Union[str, List[int], IPAddress], netmask: int=32) -> None:
  121. """
  122. Construct a ip-block given a ip-address and a netmask. Given an ip and a netmask,
  123. the constructed ip-block will describe the range ip/netmask (e.g. 127.0.0.1/8)
  124. :param ip: An ip-address, represented as IPAddress, string or 4-element-list
  125. """
  126. if isinstance(ip, str):
  127. ip = IPAddress.parse(ip)
  128. elif isinstance(ip, list):
  129. ip = IPAddress(ip)
  130. if not 1 <= netmask <= 32:
  131. raise ValueError("netmask must lie between 1 and 32")
  132. # 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
  133. self.ipnum = ip.to_int() & self._bitmask(netmask)
  134. self.netmask = netmask
  135. @staticmethod
  136. def parse(cidr: str) -> "IPAddressBlock":
  137. """
  138. Parse a string in cidr-notation and return a IPAddressBlock describing the ip-segment
  139. If the string is not in cidr-notation a ValueError is raised
  140. """
  141. match = re.match("^" + IPAddressBlock.CIDR_REGEXP + "$", cidr)
  142. if not match:
  143. raise ValueError("%s is no valid cidr-notation" % cidr)
  144. ip = [int(match.group(i)) for i in range(1, 5)]
  145. suffix = 32 if not match.group(6) else int(match.group(6))
  146. return IPAddressBlock(ip, suffix)
  147. def block_size(self) -> int:
  148. """
  149. Return the size of the ip-address-block. E.g. the size of someip/24 is 256
  150. """
  151. return 2 ** (32 - self.netmask)
  152. def first_address(self) -> IPAddress:
  153. """
  154. Return the first ip-address of the ip-block
  155. """
  156. return IPAddress.from_int(self.ipnum)
  157. def last_address(self) -> IPAddress:
  158. """
  159. Return the last ip-address of the ip-block
  160. """
  161. return IPAddress.from_int(self.ipnum + self.block_size() - 1)
  162. def _bitmask(self, netmask: int) -> int:
  163. def ones(x: int) -> int:
  164. return (1 << x) - 1
  165. return ones(32) ^ ones(32 - netmask)
  166. def __repr__(self) -> str:
  167. """
  168. Conforming to python style-guide, eval(repr(obj)) equals obj
  169. """
  170. return "IPAddressBlock(%s, %i)" % (repr(IPAddress.from_int(self.ipnum)), self.netmask)
  171. def __str__(self) -> str:
  172. """
  173. Return a string in cidr-notation
  174. """
  175. return str(IPAddress.from_int(self.ipnum)) + "/" + str(self.netmask)
  176. def __contains__(self, ip: IPAddress) -> bool:
  177. return (ip.to_int() & self._bitmask(self.netmask)) == self.ipnum
  178. class ReservedIPBlocks:
  179. """
  180. To avoid magic values and save developers some research this class contains several constants
  181. describing special network-segments and some is_-methods to check if an ip is in the specified segment.
  182. """
  183. # a list of ip-addresses that can be used in private networks
  184. PRIVATE_IP_SEGMENTS = [
  185. IPAddressBlock.parse(block)
  186. for block in
  187. ("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")
  188. ]
  189. LOCALHOST_SEGMENT = IPAddressBlock.parse("127.0.0.0/8")
  190. MULTICAST_SEGMENT = IPAddressBlock.parse("224.0.0.0/4")
  191. RESERVED_SEGMENT = IPAddressBlock.parse("240.0.0.0/4")
  192. ZERO_CONF_SEGMENT = IPAddressBlock.parse("169.254.0.0/16")
  193. @staticmethod
  194. def is_private(ip: IPAddress) -> bool:
  195. return any(ip in block for block in ReservedIPBlocks.PRIVATE_IP_SEGMENTS)
  196. @staticmethod
  197. def get_private_segment(ip: IPAddress) -> Optional[IPAddressBlock]:
  198. if not ReservedIPBlocks.is_private(ip):
  199. raise ValueError("%s is not part of a private IP segment" % ip)
  200. for block in ReservedIPBlocks.PRIVATE_IP_SEGMENTS:
  201. if ip in block:
  202. return block
  203. return None
  204. @staticmethod
  205. def is_localhost(ip: IPAddress) -> bool:
  206. return ip in ReservedIPBlocks.LOCALHOST_SEGMENT
  207. @staticmethod
  208. def is_multicast(ip: IPAddress) -> bool:
  209. return ip in ReservedIPBlocks.MULTICAST_SEGMENT
  210. @staticmethod
  211. def is_reserved(ip: IPAddress) -> bool:
  212. return ip in ReservedIPBlocks.RESERVED_SEGMENT
  213. @staticmethod
  214. def is_zero_conf(ip: IPAddress) -> bool:
  215. return ip in ReservedIPBlocks.ZERO_CONF_SEGMENT