322 lines
12 KiB
Diff
322 lines
12 KiB
Diff
|
|
From f66e25b5f549acf66d1fb6ead13eb3cff7d09af3 Mon Sep 17 00:00:00 2001
|
||
|
|
From: Bob Halley <halley@dnspython.org>
|
||
|
|
Date: Fri, 9 Feb 2024 11:22:52 -0800
|
||
|
|
Subject: [PATCH] Address DoS via the Tudoor mechanism (CVE-2023-29483) (#1044)
|
||
|
|
|
||
|
|
---
|
||
|
|
dns/asyncquery.py | 45 +++++++++++++------
|
||
|
|
dns/nameserver.py | 2 +
|
||
|
|
dns/query.py | 110 +++++++++++++++++++++++++++++-----------------
|
||
|
|
3 files changed, 103 insertions(+), 54 deletions(-)
|
||
|
|
|
||
|
|
diff --git a/dns/asyncquery.py b/dns/asyncquery.py
|
||
|
|
index 35a355b..94cb241 100644
|
||
|
|
--- a/dns/asyncquery.py
|
||
|
|
+++ b/dns/asyncquery.py
|
||
|
|
@@ -120,6 +120,8 @@ async def receive_udp(
|
||
|
|
request_mac: Optional[bytes] = b"",
|
||
|
|
ignore_trailing: bool = False,
|
||
|
|
raise_on_truncation: bool = False,
|
||
|
|
+ ignore_errors: bool = False,
|
||
|
|
+ query: Optional[dns.message.Message] = None,
|
||
|
|
) -> Any:
|
||
|
|
"""Read a DNS message from a UDP socket.
|
||
|
|
|
||
|
|
@@ -133,22 +135,30 @@ async def receive_udp(
|
||
|
|
"""
|
||
|
|
|
||
|
|
wire = b""
|
||
|
|
- while 1:
|
||
|
|
+ while True:
|
||
|
|
(wire, from_address) = await sock.recvfrom(65535, _timeout(expiration))
|
||
|
|
- if _matches_destination(
|
||
|
|
+ if not _matches_destination(
|
||
|
|
sock.family, from_address, destination, ignore_unexpected
|
||
|
|
):
|
||
|
|
- break
|
||
|
|
- received_time = time.time()
|
||
|
|
- r = dns.message.from_wire(
|
||
|
|
- wire,
|
||
|
|
- keyring=keyring,
|
||
|
|
- request_mac=request_mac,
|
||
|
|
- one_rr_per_rrset=one_rr_per_rrset,
|
||
|
|
- ignore_trailing=ignore_trailing,
|
||
|
|
- raise_on_truncation=raise_on_truncation,
|
||
|
|
- )
|
||
|
|
- return (r, received_time, from_address)
|
||
|
|
+ continue
|
||
|
|
+ received_time = time.time()
|
||
|
|
+ try:
|
||
|
|
+ r = dns.message.from_wire(
|
||
|
|
+ wire,
|
||
|
|
+ keyring=keyring,
|
||
|
|
+ request_mac=request_mac,
|
||
|
|
+ one_rr_per_rrset=one_rr_per_rrset,
|
||
|
|
+ ignore_trailing=ignore_trailing,
|
||
|
|
+ raise_on_truncation=raise_on_truncation,
|
||
|
|
+ )
|
||
|
|
+ except Exception:
|
||
|
|
+ if ignore_errors:
|
||
|
|
+ continue
|
||
|
|
+ else:
|
||
|
|
+ raise
|
||
|
|
+ if ignore_errors and query is not None and not query.is_response(r):
|
||
|
|
+ continue
|
||
|
|
+ return (r, received_time, from_address)
|
||
|
|
|
||
|
|
|
||
|
|
async def udp(
|
||
|
|
@@ -164,6 +174,7 @@ async def udp(
|
||
|
|
raise_on_truncation: bool = False,
|
||
|
|
sock: Optional[dns.asyncbackend.DatagramSocket] = None,
|
||
|
|
backend: Optional[dns.asyncbackend.Backend] = None,
|
||
|
|
+ ignore_errors: bool = False,
|
||
|
|
) -> dns.message.Message:
|
||
|
|
"""Return the response obtained after sending a query via UDP.
|
||
|
|
|
||
|
|
@@ -205,9 +216,13 @@ async def udp(
|
||
|
|
q.mac,
|
||
|
|
ignore_trailing,
|
||
|
|
raise_on_truncation,
|
||
|
|
+ ignore_errors,
|
||
|
|
+ q,
|
||
|
|
)
|
||
|
|
r.time = received_time - begin_time
|
||
|
|
- if not q.is_response(r):
|
||
|
|
+ # We don't need to check q.is_response() if we are in ignore_errors mode
|
||
|
|
+ # as receive_udp() will have checked it.
|
||
|
|
+ if not (ignore_errors or q.is_response(r)):
|
||
|
|
raise BadResponse
|
||
|
|
return r
|
||
|
|
|
||
|
|
@@ -225,6 +240,7 @@ async def udp_with_fallback(
|
||
|
|
udp_sock: Optional[dns.asyncbackend.DatagramSocket] = None,
|
||
|
|
tcp_sock: Optional[dns.asyncbackend.StreamSocket] = None,
|
||
|
|
backend: Optional[dns.asyncbackend.Backend] = None,
|
||
|
|
+ ignore_errors: bool = False,
|
||
|
|
) -> Tuple[dns.message.Message, bool]:
|
||
|
|
"""Return the response to the query, trying UDP first and falling back
|
||
|
|
to TCP if UDP results in a truncated response.
|
||
|
|
@@ -260,6 +276,7 @@ async def udp_with_fallback(
|
||
|
|
True,
|
||
|
|
udp_sock,
|
||
|
|
backend,
|
||
|
|
+ ignore_errors,
|
||
|
|
)
|
||
|
|
return (response, False)
|
||
|
|
except dns.message.Truncated:
|
||
|
|
diff --git a/dns/nameserver.py b/dns/nameserver.py
|
||
|
|
index a1fb549..0c494c1 100644
|
||
|
|
--- a/dns/nameserver.py
|
||
|
|
+++ b/dns/nameserver.py
|
||
|
|
@@ -115,6 +115,7 @@ class Do53Nameserver(AddressAndPortNameserver):
|
||
|
|
raise_on_truncation=True,
|
||
|
|
one_rr_per_rrset=one_rr_per_rrset,
|
||
|
|
ignore_trailing=ignore_trailing,
|
||
|
|
+ ignore_errors=True,
|
||
|
|
)
|
||
|
|
return response
|
||
|
|
|
||
|
|
@@ -153,6 +154,7 @@ class Do53Nameserver(AddressAndPortNameserver):
|
||
|
|
backend=backend,
|
||
|
|
one_rr_per_rrset=one_rr_per_rrset,
|
||
|
|
ignore_trailing=ignore_trailing,
|
||
|
|
+ ignore_errors=True,
|
||
|
|
)
|
||
|
|
return response
|
||
|
|
|
||
|
|
diff --git a/dns/query.py b/dns/query.py
|
||
|
|
index d4bd6b9..bdd251e 100644
|
||
|
|
--- a/dns/query.py
|
||
|
|
+++ b/dns/query.py
|
||
|
|
@@ -569,6 +569,8 @@ def receive_udp(
|
||
|
|
request_mac: Optional[bytes] = b"",
|
||
|
|
ignore_trailing: bool = False,
|
||
|
|
raise_on_truncation: bool = False,
|
||
|
|
+ ignore_errors: bool = False,
|
||
|
|
+ query: Optional[dns.message.Message] = None,
|
||
|
|
) -> Any:
|
||
|
|
"""Read a DNS message from a UDP socket.
|
||
|
|
|
||
|
|
@@ -609,28 +611,44 @@ def receive_udp(
|
||
|
|
``(dns.message.Message, float, tuple)``
|
||
|
|
tuple of the received message, the received time, and the address where
|
||
|
|
the message arrived from.
|
||
|
|
+
|
||
|
|
+ *ignore_errors*, a ``bool``. If various format errors or response
|
||
|
|
+ mismatches occur, ignore them and keep listening for a valid response.
|
||
|
|
+ The default is ``False``.
|
||
|
|
+
|
||
|
|
+ *query*, a ``dns.message.Message`` or ``None``. If not ``None`` and
|
||
|
|
+ *ignore_errors* is ``True``, check that the received message is a response
|
||
|
|
+ to this query, and if not keep listening for a valid response.
|
||
|
|
"""
|
||
|
|
|
||
|
|
wire = b""
|
||
|
|
while True:
|
||
|
|
(wire, from_address) = _udp_recv(sock, 65535, expiration)
|
||
|
|
- if _matches_destination(
|
||
|
|
+ if not _matches_destination(
|
||
|
|
sock.family, from_address, destination, ignore_unexpected
|
||
|
|
):
|
||
|
|
- break
|
||
|
|
- received_time = time.time()
|
||
|
|
- r = dns.message.from_wire(
|
||
|
|
- wire,
|
||
|
|
- keyring=keyring,
|
||
|
|
- request_mac=request_mac,
|
||
|
|
- one_rr_per_rrset=one_rr_per_rrset,
|
||
|
|
- ignore_trailing=ignore_trailing,
|
||
|
|
- raise_on_truncation=raise_on_truncation,
|
||
|
|
- )
|
||
|
|
- if destination:
|
||
|
|
- return (r, received_time)
|
||
|
|
- else:
|
||
|
|
- return (r, received_time, from_address)
|
||
|
|
+ continue
|
||
|
|
+ received_time = time.time()
|
||
|
|
+ try:
|
||
|
|
+ r = dns.message.from_wire(
|
||
|
|
+ wire,
|
||
|
|
+ keyring=keyring,
|
||
|
|
+ request_mac=request_mac,
|
||
|
|
+ one_rr_per_rrset=one_rr_per_rrset,
|
||
|
|
+ ignore_trailing=ignore_trailing,
|
||
|
|
+ raise_on_truncation=raise_on_truncation,
|
||
|
|
+ )
|
||
|
|
+ except Exception:
|
||
|
|
+ if ignore_errors:
|
||
|
|
+ continue
|
||
|
|
+ else:
|
||
|
|
+ raise
|
||
|
|
+ if ignore_errors and query is not None and not query.is_response(r):
|
||
|
|
+ continue
|
||
|
|
+ if destination:
|
||
|
|
+ return (r, received_time)
|
||
|
|
+ else:
|
||
|
|
+ return (r, received_time, from_address)
|
||
|
|
|
||
|
|
|
||
|
|
def udp(
|
||
|
|
@@ -645,6 +663,7 @@ def udp(
|
||
|
|
ignore_trailing: bool = False,
|
||
|
|
raise_on_truncation: bool = False,
|
||
|
|
sock: Optional[Any] = None,
|
||
|
|
+ ignore_errors: bool = False,
|
||
|
|
) -> dns.message.Message:
|
||
|
|
"""Return the response obtained after sending a query via UDP.
|
||
|
|
|
||
|
|
@@ -681,6 +700,10 @@ def udp(
|
||
|
|
if a socket is provided, it must be a nonblocking datagram socket,
|
||
|
|
and the *source* and *source_port* are ignored.
|
||
|
|
|
||
|
|
+ *ignore_errors*, a ``bool``. If various format errors or response
|
||
|
|
+ mismatches occur, ignore them and keep listening for a valid response.
|
||
|
|
+ The default is ``False``.
|
||
|
|
+
|
||
|
|
Returns a ``dns.message.Message``.
|
||
|
|
"""
|
||
|
|
|
||
|
|
@@ -705,9 +728,13 @@ def udp(
|
||
|
|
q.mac,
|
||
|
|
ignore_trailing,
|
||
|
|
raise_on_truncation,
|
||
|
|
+ ignore_errors,
|
||
|
|
+ q,
|
||
|
|
)
|
||
|
|
r.time = received_time - begin_time
|
||
|
|
- if not q.is_response(r):
|
||
|
|
+ # We don't need to check q.is_response() if we are in ignore_errors mode
|
||
|
|
+ # as receive_udp() will have checked it.
|
||
|
|
+ if not (ignore_errors or q.is_response(r)):
|
||
|
|
raise BadResponse
|
||
|
|
return r
|
||
|
|
assert (
|
||
|
|
@@ -727,48 +754,50 @@ def udp_with_fallback(
|
||
|
|
ignore_trailing: bool = False,
|
||
|
|
udp_sock: Optional[Any] = None,
|
||
|
|
tcp_sock: Optional[Any] = None,
|
||
|
|
+ ignore_errors: bool = False,
|
||
|
|
) -> Tuple[dns.message.Message, bool]:
|
||
|
|
"""Return the response to the query, trying UDP first and falling back
|
||
|
|
to TCP if UDP results in a truncated response.
|
||
|
|
|
||
|
|
*q*, a ``dns.message.Message``, the query to send
|
||
|
|
|
||
|
|
- *where*, a ``str`` containing an IPv4 or IPv6 address, where
|
||
|
|
- to send the message.
|
||
|
|
+ *where*, a ``str`` containing an IPv4 or IPv6 address, where to send the message.
|
||
|
|
|
||
|
|
- *timeout*, a ``float`` or ``None``, the number of seconds to wait before the
|
||
|
|
- query times out. If ``None``, the default, wait forever.
|
||
|
|
+ *timeout*, a ``float`` or ``None``, the number of seconds to wait before the query
|
||
|
|
+ times out. If ``None``, the default, wait forever.
|
||
|
|
|
||
|
|
*port*, an ``int``, the port send the message to. The default is 53.
|
||
|
|
|
||
|
|
- *source*, a ``str`` containing an IPv4 or IPv6 address, specifying
|
||
|
|
- the source address. The default is the wildcard address.
|
||
|
|
+ *source*, a ``str`` containing an IPv4 or IPv6 address, specifying the source
|
||
|
|
+ address. The default is the wildcard address.
|
||
|
|
|
||
|
|
- *source_port*, an ``int``, the port from which to send the message.
|
||
|
|
- The default is 0.
|
||
|
|
+ *source_port*, an ``int``, the port from which to send the message. The default is
|
||
|
|
+ 0.
|
||
|
|
|
||
|
|
- *ignore_unexpected*, a ``bool``. If ``True``, ignore responses from
|
||
|
|
- unexpected sources.
|
||
|
|
+ *ignore_unexpected*, a ``bool``. If ``True``, ignore responses from unexpected
|
||
|
|
+ sources.
|
||
|
|
|
||
|
|
- *one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own
|
||
|
|
- RRset.
|
||
|
|
+ *one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own RRset.
|
||
|
|
|
||
|
|
- *ignore_trailing*, a ``bool``. If ``True``, ignore trailing
|
||
|
|
- junk at end of the received message.
|
||
|
|
+ *ignore_trailing*, a ``bool``. If ``True``, ignore trailing junk at end of the
|
||
|
|
+ received message.
|
||
|
|
|
||
|
|
- *udp_sock*, a ``socket.socket``, or ``None``, the socket to use for the
|
||
|
|
- UDP query. If ``None``, the default, a socket is created. Note that
|
||
|
|
- if a socket is provided, it must be a nonblocking datagram socket,
|
||
|
|
- and the *source* and *source_port* are ignored for the UDP query.
|
||
|
|
+ *udp_sock*, a ``socket.socket``, or ``None``, the socket to use for the UDP query.
|
||
|
|
+ If ``None``, the default, a socket is created. Note that if a socket is provided,
|
||
|
|
+ it must be a nonblocking datagram socket, and the *source* and *source_port* are
|
||
|
|
+ ignored for the UDP query.
|
||
|
|
|
||
|
|
*tcp_sock*, a ``socket.socket``, or ``None``, the connected socket to use for the
|
||
|
|
- TCP query. If ``None``, the default, a socket is created. Note that
|
||
|
|
- if a socket is provided, it must be a nonblocking connected stream
|
||
|
|
- socket, and *where*, *source* and *source_port* are ignored for the TCP
|
||
|
|
- query.
|
||
|
|
+ TCP query. If ``None``, the default, a socket is created. Note that if a socket is
|
||
|
|
+ provided, it must be a nonblocking connected stream socket, and *where*, *source*
|
||
|
|
+ and *source_port* are ignored for the TCP query.
|
||
|
|
+
|
||
|
|
+ *ignore_errors*, a ``bool``. If various format errors or response mismatches occur
|
||
|
|
+ while listening for UDP, ignore them and keep listening for a valid response. The
|
||
|
|
+ default is ``False``.
|
||
|
|
|
||
|
|
- Returns a (``dns.message.Message``, tcp) tuple where tcp is ``True``
|
||
|
|
- if and only if TCP was used.
|
||
|
|
+ Returns a (``dns.message.Message``, tcp) tuple where tcp is ``True`` if and only if
|
||
|
|
+ TCP was used.
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
response = udp(
|
||
|
|
@@ -783,6 +812,7 @@ def udp_with_fallback(
|
||
|
|
ignore_trailing,
|
||
|
|
True,
|
||
|
|
udp_sock,
|
||
|
|
+ ignore_errors,
|
||
|
|
)
|
||
|
|
return (response, False)
|
||
|
|
except dns.message.Truncated:
|
||
|
|
--
|
||
|
|
2.39.1
|
||
|
|
|
||
|
|
|