import six import netifaces import netaddr # new classes: to-be- pasted into ipapython/ipautil class UnsafeIPAddress(netaddr.IPAddress): """Any valid IP address with or without netmask.""" # Use inet_pton() rather than inet_aton() for IP address parsing. We # will use the same function in IPv4/IPv6 conversions + be stricter # and don't allow IP addresses such as '1.1.1' in the same time netaddr_ip_flags = netaddr.INET_PTON def __init__(self, addr): if isinstance(addr, UnsafeIPAddress): self._net = addr._net super(UnsafeIPAddress, self).__init__(addr, flags=self.netaddr_ip_flags) return elif isinstance(addr, netaddr.IPAddress): self._net = None # no information about netmask super(UnsafeIPAddress, self).__init__(addr, flags=self.netaddr_ip_flags) return elif isinstance(addr, netaddr.IPNetwork): self._net = addr super(UnsafeIPAddress, self).__init__(self._net.ip, flags=self.netaddr_ip_flags) return # option of last resort: parse it as string self._net = None addr = str(addr) try: try: addr = netaddr.IPAddress(addr, flags=self.netaddr_ip_flags) except netaddr.AddrFormatError: # netaddr.IPAddress doesn't handle zone indices in textual # IPv6 addresses. Try removing zone index and parse the # address again. addr, sep, foo = addr.partition('%') if sep != '%': raise addr = netaddr.IPAddress(addr, flags=self.netaddr_ip_flags) if addr.version != 6: raise except ValueError: self._net = netaddr.IPNetwork(addr, flags=self.netaddr_ip_flags) addr = self._net.ip super(UnsafeIPAddress, self).__init__(addr, flags=self.netaddr_ip_flags) class CheckedIPAddress(UnsafeIPAddress): """IPv4 or IPv6 address with additional constraints. Reserved or link-local addresses are never accepted. """ def __init__(self, addr, match_local=False, parse_netmask=True, allow_network=False, allow_loopback=False, allow_broadcast=False, allow_multicast=False): super(CheckedIPAddress, self).__init__(addr) if isinstance(addr, CheckedIPAddress): self.prefixlen = addr.prefixlen return if not parse_netmask and self._net: raise ValueError( "netmask and prefix length not allowed here: {}".format(addr)) if self.version not in (4, 6): raise ValueError("unsupported IP version {}".format(self.version)) if not allow_loopback and self.is_loopback(): raise ValueError("cannot use loopback IP address {}".format(addr)) if (not self.is_loopback() and self.is_reserved()) \ or self in netaddr.ip.IPV4_6TO4: raise ValueError( "cannot use IANA reserved IP address {}".format(addr)) if self.is_link_local(): raise ValueError( "cannot use link-local IP address {}".format(addr)) if not allow_multicast and self.is_multicast(): raise ValueError("cannot use multicast IP address {}".format(addr)) if match_local: if self.version == 4: family = netifaces.AF_INET elif self.version == 6: family = netifaces.AF_INET6 else: raise ValueError( "Unsupported address family ({})".format(self.version) ) iface = None for interface in netifaces.interfaces(): for ifdata in netifaces.ifaddresses(interface).get(family, []): ifnet = netaddr.IPNetwork('{addr}/{netmask}'.format( addr=ifdata['addr'], netmask=ifdata['netmask'] )) if ifnet == self._net or ( self._net is None and ifnet.ip == self): self._net = ifnet iface = interface break if iface is None: raise ValueError('no network interface matches the IP address ' 'and netmask {}'.format(addr)) if self._net is None: if self.version == 4: self._net = netaddr.IPNetwork( netaddr.cidr_abbrev_to_verbose(str(self))) elif self.version == 6: self._net = netaddr.IPNetwork(str(self) + '/64') if not allow_network and self == self._net.network: raise ValueError("cannot use IP network address {}".format(addr)) if not allow_broadcast and (self.version == 4 and self == self._net.broadcast): raise ValueError("cannot use broadcast IP address {}".format(addr)) self.prefixlen = self._net.prefixlen # original class: copy & paste from ipapython/ipautil class CheckedIPAddressOld(netaddr.IPAddress): # Use inet_pton() rather than inet_aton() for IP address parsing. We # will use the same function in IPv4/IPv6 conversions + be stricter # and don't allow IP addresses such as '1.1.1' in the same time netaddr_ip_flags = netaddr.INET_PTON def __init__(self, addr, match_local=False, parse_netmask=True, allow_network=False, allow_loopback=False, allow_broadcast=False, allow_multicast=False): if isinstance(addr, CheckedIPAddress): super(CheckedIPAddressOld, self).__init__(addr, flags=self.netaddr_ip_flags) self.prefixlen = addr.prefixlen return net = None iface = None if isinstance(addr, netaddr.IPNetwork): net = addr addr = net.ip elif isinstance(addr, netaddr.IPAddress): pass else: try: try: addr = netaddr.IPAddress(str(addr), flags=self.netaddr_ip_flags) except netaddr.AddrFormatError: # netaddr.IPAddress doesn't handle zone indices in textual # IPv6 addresses. Try removing zone index and parse the # address again. if not isinstance(addr, six.string_types): raise addr, sep, foo = addr.partition('%') if sep != '%': raise addr = netaddr.IPAddress(str(addr), flags=self.netaddr_ip_flags) if addr.version != 6: raise except ValueError: net = netaddr.IPNetwork(str(addr), flags=self.netaddr_ip_flags) if not parse_netmask: raise ValueError("netmask and prefix length not allowed here") addr = net.ip if addr.version not in (4, 6): raise ValueError("unsupported IP version") if not allow_loopback and addr.is_loopback(): raise ValueError("cannot use loopback IP address") if (not addr.is_loopback() and addr.is_reserved()) \ or addr in netaddr.ip.IPV4_6TO4: raise ValueError("cannot use IANA reserved IP address") if addr.is_link_local(): raise ValueError("cannot use link-local IP address") if not allow_multicast and addr.is_multicast(): raise ValueError("cannot use multicast IP address") if match_local: if addr.version == 4: family = netifaces.AF_INET elif addr.version == 6: family = netifaces.AF_INET6 else: raise ValueError( "Unsupported address family ({})".format(addr.version) ) for interface in netifaces.interfaces(): for ifdata in netifaces.ifaddresses(interface).get(family, []): ifnet = netaddr.IPNetwork('{addr}/{netmask}'.format( addr=ifdata['addr'], netmask=ifdata['netmask'] )) if ifnet == net or (net is None and ifnet.ip == addr): net = ifnet iface = interface break if iface is None: raise ValueError('No network interface matches the provided IP address and netmask') if net is None: if addr.version == 4: net = netaddr.IPNetwork(netaddr.cidr_abbrev_to_verbose(str(addr))) elif addr.version == 6: net = netaddr.IPNetwork(str(addr) + '/64') if not allow_network and addr == net.network: raise ValueError("cannot use IP network address") if not allow_broadcast and addr.version == 4 and addr == net.broadcast: raise ValueError("cannot use broadcast IP address") super(CheckedIPAddressOld, self).__init__(addr, flags=self.netaddr_ip_flags) self.prefixlen = net.prefixlen from hypothesis import given, assume, example, settings from hypothesis.strategies import text, integers, composite, booleans def get_ipv4_mask(draw): addr = "/" if draw(booleans()): # want mask addr += str(draw(integers(min_value=-1, max_value=130))) else: addr += str(get_ipv4(draw)) return addr def get_ipv4(draw): b1 = draw(integers(min_value=0, max_value=256)) b2 = draw(integers(min_value=0, max_value=256)) b3 = draw(integers(min_value=0, max_value=256)) b4 = draw(integers(min_value=0, max_value=256)) addr = "%s.%s.%s.%s" % (b1, b2, b3, b4) if draw(booleans()): # want mask addr += get_ipv4_mask(draw) return addr def get_ipv6_mask(draw): addr = "/" addr += str(draw(integers(min_value=-1, max_value=130))) return addr def get_ipv6(draw): b1 = draw(integers(min_value=2000, max_value=3000)) b2 = draw(integers(min_value=0, max_value=255)) b3 = draw(integers(min_value=0, max_value=255)) b4 = draw(integers(min_value=0, max_value=255)) addr = "%s:%s::%s:%s" % (b1, b2, b3, b4) if draw(booleans()): # want mask addr += get_ipv6_mask(draw) return addr @composite def get_address(draw): while True: if draw(booleans()): # want mask addr = get_ipv6(draw) else: addr = get_ipv4(draw) i = draw(integers(min_value=0, max_value=2)) try: if i == 0: return netaddr.IPAddress(addr) elif i == 1: return netaddr.IPNetwork(addr) else: return addr except: pass # test localhost @example('127.0.0.1', False, True, False, True, False, False) @example('127.0.0.1', False, True, False, False, False, False) @example('127.0.0.1', True, True, False, True, False, False) @example('127.0.0.2', False, True, False, True, False, False) @example('127.0.0.2', True, True, False, True, False, False) # generate random addresses @given(addr=get_address(), match_local=booleans(), parse_netmask=booleans(), allow_network=booleans(), allow_loopback=booleans(), allow_broadcast=booleans(), allow_multicast=booleans()) # generate A LOT of addresses @settings(max_examples=10000) def compare_old_and_new_implementations(addr, match_local, parse_netmask, allow_network, allow_loopback, allow_broadcast, allow_multicast): debug = False if debug: print('addr: %s %s %s %s %s %s %s %s' % (type(addr), addr, match_local, parse_netmask, allow_network, allow_loopback, allow_broadcast, allow_multicast)) if isinstance(addr, netaddr.IPNetwork): # it does not make sense to parse IP network object # and set parse_netmask=False! # The old implementation was accepting this input # but it was not used in the IPA code so I've removed this corner case assume(parse_netmask is True) unsafeip = None unsafeex = None try: unsafeip = UnsafeIPAddress(addr) except Exception as unsafeex: pass # compare outputs and exceptions thrown by both implementations oldip = None oldex = None try: oldip = CheckedIPAddressOld(addr, match_local, parse_netmask, allow_network, allow_loopback, allow_broadcast, allow_multicast) except Exception as oldex: pass newip = None newex = None try: newip = CheckedIPAddress(addr, match_local, parse_netmask, allow_network, allow_loopback, allow_broadcast, allow_multicast) if debug: print(' OK input accepted') except Exception as newex: pass if newip: assert unsafeip, ( 'unsafeip %s is not superset of newip %s' % (unsafeip, newip)) assert type(oldex) == type(newex), ( '\noldex: %s %s\n !=\nnewex: %s %s' % ( type(oldex), oldex, type(newex), newex)) assert oldip == newip compare_old_and_new_implementations()