Skip to content

Commit 3e7dba6

Browse files
committed
gh-74713: Lib/ipaddress: support reverse pointer generation for networks
Before this change, DNS reverse pointer generation for network objects produced incorrect results, such as: >>> ipaddress.IPv4Network('192.168.1.0/24').reverse_pointer '0/24.1.168.192.in-addr.arpa' >>> ipaddress.IPv6Network('2001:db8:1234::/48').reverse_pointer '8.4./.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.4.3.2.1.8.b.d.0.1.0.0.2.ip6.arpa' This change introduces a more generalised reverse pointer generation algorithm, suitable for both address and network objects. The same code above, after applying this change: >>> ipaddress.IPv4Network('192.168.1.0/24').reverse_pointer '1.168.192.in-addr.arpa' >>> ipaddress.IPv6Network('2001:db8:1234::/48').reverse_pointer '4.3.2.1.8.b.d.0.1.0.0.2.ip6.arpa' Getting a reverse pointer for a network, whose prefix size can't be exactly represented in a reverse pointer record, will raise an exception: >>> ipaddress.IPv4Network('192.168.1.0/28').reverse_pointer ipaddress.NetmaskValueError: Reverse pointer cannot be generated for given prefix size >>> ipaddress.IPv6Network('2001:db8:1234::/50').reverse_pointer ipaddress.NetmaskValueError: Reverse pointer cannot be generated for given prefix size
1 parent b6854c5 commit 3e7dba6

3 files changed

Lines changed: 58 additions & 6 deletions

File tree

Lib/ipaddress.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,13 +1252,20 @@ def _string_from_ip_int(cls, ip_int):
12521252
return '.'.join(map(str, ip_int.to_bytes(4, 'big')))
12531253

12541254
def _reverse_pointer(self):
1255-
"""Return the reverse DNS pointer name for the IPv4 address.
1255+
"""Return the reverse DNS pointer name for the IPv4 address or network.
12561256
12571257
This implements the method described in RFC1035 3.5.
12581258
12591259
"""
1260-
reverse_octets = str(self).split('.')[::-1]
1261-
return '.'.join(reverse_octets) + '.in-addr.arpa'
1260+
prefix = getattr(self, 'prefixlen', IPV4LENGTH)
1261+
octet_count, remainder = divmod(prefix, 8) # each octet is 8 bits long
1262+
if remainder:
1263+
raise NetmaskValueError(
1264+
'Reverse pointer cannot be generated for given prefix size')
1265+
address_exploded = getattr(self, 'network_address', self).exploded
1266+
reverse_octets = address_exploded.split('.')[:octet_count][::-1]
1267+
reverse_octets.append('in-addr.arpa')
1268+
return '.'.join(reverse_octets)
12621269

12631270
class IPv4Address(_BaseV4, _BaseAddress):
12641271

@@ -1880,13 +1887,20 @@ def _explode_shorthand_ip_string(self):
18801887
return ':'.join(parts)
18811888

18821889
def _reverse_pointer(self):
1883-
"""Return the reverse DNS pointer name for the IPv6 address.
1890+
"""Return the reverse DNS pointer name for the IPv6 address or network.
18841891
18851892
This implements the method described in RFC3596 2.5.
18861893
18871894
"""
1888-
reverse_chars = self.exploded[::-1].replace(':', '')
1889-
return '.'.join(reverse_chars) + '.ip6.arpa'
1895+
prefix = getattr(self, 'prefixlen', IPV6LENGTH)
1896+
char_count, remainder = divmod(prefix, 4) # each char is 4 bits long
1897+
if remainder:
1898+
raise NetmaskValueError(
1899+
'Reverse pointer cannot be generated for given prefix size')
1900+
address_exploded = getattr(self, 'network_address', self).exploded
1901+
reverse_chars = list(address_exploded.replace(':', '')[:char_count][::-1])
1902+
reverse_chars.append('ip6.arpa')
1903+
return '.'.join(reverse_chars)
18901904

18911905
@staticmethod
18921906
def _split_scope_id(ip_str):

Lib/test/test_ipaddress.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,24 @@ def test_subnet_of_mixed_types(self):
723723
ipaddress.IPv6Network('::1/128').subnet_of(
724724
ipaddress.IPv4Network('10.0.0.0/30'))
725725

726+
def test_reverse_pointer(self):
727+
self.assertEqual(self.factory('0.0.0.0/0').reverse_pointer,
728+
'in-addr.arpa')
729+
self.assertEqual(self.factory('127.0.0.0/8').reverse_pointer,
730+
'127.in-addr.arpa')
731+
self.assertEqual(self.factory('127.12.0.0/16').reverse_pointer,
732+
'12.127.in-addr.arpa')
733+
self.assertEqual(self.factory('127.12.34.0/24').reverse_pointer,
734+
'34.12.127.in-addr.arpa')
735+
self.assertEqual(self.factory('127.12.34.56/32').reverse_pointer,
736+
'56.34.12.127.in-addr.arpa')
737+
738+
with self.assertRaises(ipaddress.NetmaskValueError):
739+
self.factory('127.0.0.0/12').reverse_pointer
740+
741+
with self.assertRaises(ipaddress.NetmaskValueError):
742+
self.factory('127.23.0.0/17').reverse_pointer
743+
726744

727745
class NetmaskTestMixin_v6(CommonTestMixin_v6):
728746
"""Input validation on interfaces and networks is very similar"""
@@ -879,6 +897,24 @@ def test_supernet_of(self):
879897
self.factory('2000:aaa::/48').supernet_of(
880898
self.factory('2000:aaa::/56')))
881899

900+
def test_reverse_pointer(self):
901+
self.assertEqual(self.factory('::/0').reverse_pointer, 'ip6.arpa')
902+
self.assertEqual(self.factory('2000::/4').reverse_pointer, '2.ip6.arpa')
903+
self.assertEqual(self.factory('2000::/8').reverse_pointer,
904+
'0.2.ip6.arpa')
905+
self.assertEqual(self.factory('2001:db8::/32').reverse_pointer,
906+
'8.b.d.0.1.0.0.2.ip6.arpa')
907+
self.assertEqual(self.factory('2001:db8:1234:5678::/64').reverse_pointer,
908+
'8.7.6.5.4.3.2.1.8.b.d.0.1.0.0.2.ip6.arpa')
909+
self.assertEqual(self.factory('2001:db8::1/128').reverse_pointer,
910+
'1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0' +
911+
'.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa')
912+
913+
with self.assertRaises(ipaddress.NetmaskValueError):
914+
self.factory('2000::/7').reverse_pointer
915+
916+
with self.assertRaises(ipaddress.NetmaskValueError):
917+
self.factory('2001:db8::10/127').reverse_pointer
882918

883919
class FactoryFunctionErrors(BaseTestCase):
884920

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix reverse pointer generation for :class:`ipaddress.IPv4Network` and
2+
:class:`ipaddress.IPv6Network` objects.

0 commit comments

Comments
 (0)