From: Bob Halley Date: Thu, 16 Nov 2023 00:47:52 +0000 (-0800) Subject: Canonicalize IPV4 and IPv6 address text form in rdata. (#1013) X-Git-Tag: v2.5.0rc1~27 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=cbb39a95dcf6caec9b85853b9b0ccd783ebc300e;p=thirdparty%2Fdnspython.git Canonicalize IPV4 and IPv6 address text form in rdata. (#1013) --- diff --git a/dns/inet.py b/dns/inet.py index 02e925c6..4a03f996 100644 --- a/dns/inet.py +++ b/dns/inet.py @@ -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 diff --git a/dns/ipv4.py b/dns/ipv4.py index f549150a..65ee69c0 100644 --- a/dns/ipv4.py +++ b/dns/ipv4.py @@ -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)) diff --git a/dns/ipv6.py b/dns/ipv6.py index 0cc3d868..44a10639 100644 --- a/dns/ipv6.py +++ b/dns/ipv6.py @@ -104,7 +104,7 @@ _colon_colon_end = re.compile(rb".*::$") 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. @@ -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)) diff --git a/dns/rdata.py b/dns/rdata.py index 0d262e8d..5482e71d 100644 --- a/dns/rdata.py +++ b/dns/rdata.py @@ -547,9 +547,7 @@ class Rdata: @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: @@ -558,9 +556,7 @@ class Rdata: @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: diff --git a/tests/example1.good b/tests/example1.good index 6d38a21d..045365c0 100644 --- a/tests/example1.good +++ b/tests/example1.good @@ -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 diff --git a/tests/example2.good b/tests/example2.good index 8548a3d8..5bb13768 100644 --- a/tests/example2.good +++ b/tests/example2.good @@ -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 diff --git a/tests/example3.good b/tests/example3.good index 6d38a21d..045365c0 100644 --- a/tests/example3.good +++ b/tests/example3.good @@ -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 diff --git a/tests/example4.good b/tests/example4.good index befbcc9f..3c154a0b 100644 --- a/tests/example4.good +++ b/tests/example4.good @@ -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 diff --git a/tests/test_ntoaaton.py b/tests/test_ntoaaton.py index 94386cec..9ffafb0a 100644 --- a/tests/test_ntoaaton.py +++ b/tests/test_ntoaaton.py @@ -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 @@ -41,6 +42,27 @@ v4_bad_addrs = [ "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): @@ -350,6 +372,30 @@ class NtoAAtoNTestCase(unittest.TestCase): 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()