Skip to content

Commit

Permalink
Canonicalize IPV4 and IPv6 address text form in rdata.
Browse files Browse the repository at this point in the history
  • Loading branch information
rthalley committed Nov 15, 2023
1 parent 12ac37c commit 3fbf4d8
Show file tree
Hide file tree
Showing 9 changed files with 96 additions and 13 deletions.
17 changes: 17 additions & 0 deletions dns/inet.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,20 @@ def any_for_af(af):
elif af == socket.AF_INET6:
return "::"
raise NotImplementedError(f"unknown address family {af}")


def canonicalize(text: str) -> str:
"""Verify that *address* is a valid text form IPv4 or IPv6 address and return its
canonical text form. IPv6 addresses with scopes are rejected.
*text*, a ``str``, the address in textual form.
Raises ``ValueError`` if the text is not valid.
"""
try:
return dns.ipv6.canonicalize(text)
except Exception:
try:
return dns.ipv4.canonicalize(text)
except Exception:
raise ValueError
13 changes: 13 additions & 0 deletions dns/ipv4.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,16 @@ def inet_aton(text: Union[str, bytes]) -> bytes:
return struct.pack("BBBB", *b)
except Exception:
raise dns.exception.SyntaxError


def canonicalize(text: Union[str, bytes]) -> str:
"""Verify that *address* is a valid text form IPv4 address and return its
canonical text form.
*text*, a ``str`` or ``bytes``, the IPv4 address in textual form.
Raises ``dns.exception.SyntaxError`` if the text is not valid.
"""
# Note that inet_aton() only accepts canonial form, but we still run through
# inet_ntoa() to ensure the output is a str.
return dns.ipv4.inet_ntoa(dns.ipv4.inet_aton(text))
13 changes: 12 additions & 1 deletion dns/ipv6.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def inet_ntoa(address: bytes) -> str:
def inet_aton(text: Union[str, bytes], ignore_scope: bool = False) -> bytes:
"""Convert an IPv6 address in text form to binary form.
*text*, a ``str``, the IPv6 address in textual form.
*text*, a ``str`` or ``bytes``, the IPv6 address in textual form.
*ignore_scope*, a ``bool``. If ``True``, a scope will be ignored.
If ``False``, the default, it is an error for a scope to be present.
Expand Down Expand Up @@ -206,3 +206,14 @@ def is_mapped(address: bytes) -> bool:
"""

return address.startswith(_mapped_prefix)


def canonicalize(text: Union[str, bytes]) -> str:
"""Verify that *address* is a valid text form IPv6 address and return its
canonical text form. Addresses with scopes are rejected.
*text*, a ``str`` or ``bytes``, the IPv6 address in textual form.
Raises ``dns.exception.SyntaxError`` if the text is not valid.
"""
return dns.ipv6.inet_ntoa(dns.ipv6.inet_aton(text))
8 changes: 2 additions & 6 deletions dns/rdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -547,9 +547,7 @@ def _as_int(cls, value, low=None, high=None):
@classmethod
def _as_ipv4_address(cls, value):
if isinstance(value, str):
# call to check validity
dns.ipv4.inet_aton(value)
return value
return dns.ipv4.canonicalize(value)
elif isinstance(value, bytes):
return dns.ipv4.inet_ntoa(value)
else:
Expand All @@ -558,9 +556,7 @@ def _as_ipv4_address(cls, value):
@classmethod
def _as_ipv6_address(cls, value):
if isinstance(value, str):
# call to check validity
dns.ipv6.inet_aton(value)
return value
return dns.ipv6.canonicalize(value)
elif isinstance(value, bytes):
return dns.ipv6.inet_ntoa(value)
else:
Expand Down
2 changes: 1 addition & 1 deletion tests/example1.good
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ amtrelay03 3600 IN AMTRELAY 10 0 1 203.0.113.15
amtrelay04 3600 IN AMTRELAY 10 0 2 2001:db8::15
amtrelay05 3600 IN AMTRELAY 128 1 3 amtrelays.example.com.
apl01 3600 IN APL 1:192.168.32.0/21 !1:192.168.38.0/28
apl02 3600 IN APL 1:224.0.0.0/4 2:FF00:0:0:0:0:0:0:0/8
apl02 3600 IN APL 1:224.0.0.0/4 2:ff00::/8
avc01 3600 IN AVC "app-name:WOLFGANG|app-class:OAM|business=yes"
b 300 IN CNAME foo.net.
c 300 IN A 73.80.65.49
Expand Down
2 changes: 1 addition & 1 deletion tests/example2.good
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ amtrelay03.example. 3600 IN AMTRELAY 10 0 1 203.0.113.15
amtrelay04.example. 3600 IN AMTRELAY 10 0 2 2001:db8::15
amtrelay05.example. 3600 IN AMTRELAY 128 1 3 amtrelays.example.com.
apl01.example. 3600 IN APL 1:192.168.32.0/21 !1:192.168.38.0/28
apl02.example. 3600 IN APL 1:224.0.0.0/4 2:FF00:0:0:0:0:0:0:0/8
apl02.example. 3600 IN APL 1:224.0.0.0/4 2:ff00::/8
avc01.example. 3600 IN AVC "app-name:WOLFGANG|app-class:OAM|business=yes"
b.example. 300 IN CNAME foo.net.
c.example. 300 IN A 73.80.65.49
Expand Down
2 changes: 1 addition & 1 deletion tests/example3.good
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ amtrelay03 3600 IN AMTRELAY 10 0 1 203.0.113.15
amtrelay04 3600 IN AMTRELAY 10 0 2 2001:db8::15
amtrelay05 3600 IN AMTRELAY 128 1 3 amtrelays.example.com.
apl01 3600 IN APL 1:192.168.32.0/21 !1:192.168.38.0/28
apl02 3600 IN APL 1:224.0.0.0/4 2:FF00:0:0:0:0:0:0:0/8
apl02 3600 IN APL 1:224.0.0.0/4 2:ff00::/8
avc01 3600 IN AVC "app-name:WOLFGANG|app-class:OAM|business=yes"
b 300 IN CNAME foo.net.
c 300 IN A 73.80.65.49
Expand Down
2 changes: 1 addition & 1 deletion tests/example4.good
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ amtrelay03 3600 IN AMTRELAY 10 0 1 203.0.113.15
amtrelay04 3600 IN AMTRELAY 10 0 2 2001:db8::15
amtrelay05 3600 IN AMTRELAY 128 1 3 amtrelays.example.com.
apl01 3600 IN APL 1:192.168.32.0/21 !1:192.168.38.0/28
apl02 3600 IN APL 1:224.0.0.0/4 2:FF00:0:0:0:0:0:0:0/8
apl02 3600 IN APL 1:224.0.0.0/4 2:ff00::/8
avc01 3600 IN AVC "app-name:WOLFGANG|app-class:OAM|business=yes"
b 300 IN CNAME foo.net.
c 300 IN A 73.80.65.49
Expand Down
50 changes: 48 additions & 2 deletions tests/test_ntoaaton.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

import unittest
import binascii
import itertools
import socket
import unittest

import dns.exception
import dns.inet
import dns.ipv4
import dns.ipv6
import dns.inet

# for convenience
aton4 = dns.ipv4.inet_aton
Expand All @@ -41,6 +42,27 @@
"1.2.3.4.",
]

v4_canonicalize_addrs = [
# (input, expected)
("127.0.0.1", "127.0.0.1"),
(b"127.0.0.1", "127.0.0.1"),
]

v6_canonicalize_addrs = [
# (input, expected)
("2001:503:83eb:0:0:0:0:30", "2001:503:83eb::30"),
(b"2001:503:83eb:0:0:0:0:30", "2001:503:83eb::30"),
("2001:db8::1:1:1:1:1", "2001:db8:0:1:1:1:1:1"),
("2001:DB8::1:1:1:1:1", "2001:db8:0:1:1:1:1:1"),
]

bad_canonicalize_addrs = [
"127.00.0.1",
"hi there",
"2001::db8::1:1:1:1:1",
"fe80::1%lo0",
]


class NtoAAtoNTestCase(unittest.TestCase):
def test_aton1(self):
Expand Down Expand Up @@ -350,6 +372,30 @@ def test_bogus_family(self):
NotImplementedError, lambda: dns.inet.inet_ntop(12345, b"bogus")
)

def test_ipv4_canonicalize(self):
for address, expected in v4_canonicalize_addrs:
self.assertEqual(dns.ipv4.canonicalize(address), expected)
for bad_address in bad_canonicalize_addrs:
self.assertRaises(
dns.exception.SyntaxError, lambda: dns.ipv4.canonicalize(bad_address)
)

def test_ipv6_canonicalize(self):
for address, expected in v6_canonicalize_addrs:
self.assertEqual(dns.ipv6.canonicalize(address), expected)
for bad_address in bad_canonicalize_addrs:
self.assertRaises(
dns.exception.SyntaxError, lambda: dns.ipv6.canonicalize(bad_address)
)

def test_inet_canonicalize(self):
for address, expected in itertools.chain(
v4_canonicalize_addrs, v6_canonicalize_addrs
):
self.assertEqual(dns.inet.canonicalize(address), expected)
for bad_address in bad_canonicalize_addrs:
self.assertRaises(ValueError, lambda: dns.inet.canonicalize(bad_address))


if __name__ == "__main__":
unittest.main()

0 comments on commit 3fbf4d8

Please sign in to comment.