Short version:
Validation of netmasks and hostmasks in IPv4Network is wrong: it rejects many
valid netmasks, it accepts many invalid netmasks and hostmasks, and it sometimes
throws the wrong exception. Patch attached.
Long version:
Wrongly rejecting hostmasks
---------------------------
Creating IPv4Network objects using a hostmask only works for 3 of the 31 possible hostmasks.
It works fine for /8, /16, and /24 networks, but anything else fails. E.g. first let's
calculate the hostmask for a /21 network:
>>> from ipaddress import IPv4Network
>>> IPv4Network('0.0.0.0/21').hostmask
IPv4Address('0.0.7.255')
Then try using it:
>> IPv4Network('0.0.0.0/0.0.7.255')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "c:\Python33\lib\ipaddress.py", line 1443, in __init__
% addr[1])
ipaddress.NetmaskValueError: '0.0.7.255' is not a valid netmask
The problem is that there is a list of "_valid_mask_octets". Although the values listed
are correct for netmasks, they are wrong for host masks. In binary, a netmask has 1s
in the most significant <prefixlen> bits and 0s everywhere else; a hostmask has 0s
in the most significant <prefixlen> bits and 1s everywhere else. So netmasks have
octet values 0b11111111, 0b11111110, 0b11111100, etc, whereas hostmasks have
octet values 0b11111111, 0b01111111, 0b00111111, etc.
Wrongly accepting hostmasks
---------------------------
Once the individual octets have been validated, the hostmask validation just checks
the first octet is less than the last octet. This accepts values like "0.255.0.255",
which is not a valid hostmask. The IPv4Network object then has wierd nonsensical
values.
Wrongly accepting netmasks
---------------------------
Once the individual octets have been validated, the netmask validation just checks
each octet is no greater than the one before. This accepts values like "254.192.128.0",
which is not a valid netmask. The IPv4Network object then has wierd nonsensical
values.
Inconsistent parsing
--------------------
The existing validation code includes its own parsing code. If the netmask/hostmask
passes that vaildation, it then goes into _ip_int_from_string() to be parsed and
used. _ip_int_from_string() checks things that aren't caught by the validation
code, leading to AddressValueError being thrown when NetmaskValueError was expected:
>>> IPv4Network('1.2.0.0/0.0.0255.255')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "c:\Python33\lib\ipaddress.py", line 1440, in __init__
self._ip_int_from_string(addr[1]) ^ self._ALL_ONES)
File "c:\Python33\lib\ipaddress.py", line 1055, in _ip_int_from_string
raise AddressValueError("%s in %r" % (exc, ip_str)) from None
ipaddress.AddressValueError: At most 3 characters permitted in '0255' in '0.0.0255.255'
The correct fix for this one is obviously to use the same parser in all the places
we parse the netmask/hostmask.
The patch
---------
I'm attaching a patch, with tests, that fixes these issues. Reusing the existing
_ip_int_from_string() function to parse the netmask/hostmask simplified the
validation code a lot.
My hope is that this patch is suitable for a backport to 3.3, as well as trunk.
|