Skip to content

Commit 630f24a

Browse files
committed
gh-issue-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 8be8949 commit 630f24a

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
@@ -1249,13 +1249,20 @@ def _string_from_ip_int(cls, ip_int):
12491249
return '.'.join(map(str, ip_int.to_bytes(4, 'big')))
12501250

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

12601267
@property
12611268
def max_prefixlen(self):
@@ -1835,13 +1842,20 @@ def _explode_shorthand_ip_string(self):
18351842
return ':'.join(parts)
18361843

18371844
def _reverse_pointer(self):
1838-
"""Return the reverse DNS pointer name for the IPv6 address.
1845+
"""Return the reverse DNS pointer name for the IPv6 address or network.
18391846
18401847
This implements the method described in RFC3596 2.5.
18411848
18421849
"""
1843-
reverse_chars = self.exploded[::-1].replace(':', '')
1844-
return '.'.join(reverse_chars) + '.ip6.arpa'
1850+
prefix = getattr(self, 'prefixlen', IPV6LENGTH)
1851+
char_count, remainder = divmod(prefix, 4) # each char is 4 bits long
1852+
if remainder:
1853+
raise NetmaskValueError(
1854+
'Reverse pointer cannot be generated for given prefix size')
1855+
address_exploded = getattr(self, 'network_address', self).exploded
1856+
reverse_chars = list(address_exploded.replace(':', '')[:char_count][::-1])
1857+
reverse_chars.append('ip6.arpa')
1858+
return '.'.join(reverse_chars)
18451859

18461860
@staticmethod
18471861
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
@@ -690,6 +690,24 @@ def test_subnet_of_mixed_types(self):
690690
ipaddress.IPv6Network('::1/128').subnet_of(
691691
ipaddress.IPv4Network('10.0.0.0/30'))
692692

693+
def test_reverse_pointer(self):
694+
self.assertEqual(self.factory('0.0.0.0/0').reverse_pointer,
695+
'in-addr.arpa')
696+
self.assertEqual(self.factory('127.0.0.0/8').reverse_pointer,
697+
'127.in-addr.arpa')
698+
self.assertEqual(self.factory('127.12.0.0/16').reverse_pointer,
699+
'12.127.in-addr.arpa')
700+
self.assertEqual(self.factory('127.12.34.0/24').reverse_pointer,
701+
'34.12.127.in-addr.arpa')
702+
self.assertEqual(self.factory('127.12.34.56/32').reverse_pointer,
703+
'56.34.12.127.in-addr.arpa')
704+
705+
with self.assertRaises(ipaddress.NetmaskValueError):
706+
self.factory('127.0.0.0/12').reverse_pointer
707+
708+
with self.assertRaises(ipaddress.NetmaskValueError):
709+
self.factory('127.23.0.0/17').reverse_pointer
710+
693711

694712
class NetmaskTestMixin_v6(CommonTestMixin_v6):
695713
"""Input validation on interfaces and networks is very similar"""
@@ -842,6 +860,24 @@ def test_supernet_of(self):
842860
self.factory('2000:aaa::/48').supernet_of(
843861
self.factory('2000:aaa::/56')))
844862

863+
def test_reverse_pointer(self):
864+
self.assertEqual(self.factory('::/0').reverse_pointer, 'ip6.arpa')
865+
self.assertEqual(self.factory('2000::/4').reverse_pointer, '2.ip6.arpa')
866+
self.assertEqual(self.factory('2000::/8').reverse_pointer,
867+
'0.2.ip6.arpa')
868+
self.assertEqual(self.factory('2001:db8::/32').reverse_pointer,
869+
'8.b.d.0.1.0.0.2.ip6.arpa')
870+
self.assertEqual(self.factory('2001:db8:1234:5678::/64').reverse_pointer,
871+
'8.7.6.5.4.3.2.1.8.b.d.0.1.0.0.2.ip6.arpa')
872+
self.assertEqual(self.factory('2001:db8::1/128').reverse_pointer,
873+
'1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0' +
874+
'.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa')
875+
876+
with self.assertRaises(ipaddress.NetmaskValueError):
877+
self.factory('2000::/7').reverse_pointer
878+
879+
with self.assertRaises(ipaddress.NetmaskValueError):
880+
self.factory('2001:db8::10/127').reverse_pointer
845881

846882
class FactoryFunctionErrors(BaseTestCase):
847883

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)