Skip to content

Commit

Permalink
Address DoS via the Tudoor mechanism (CVE-2023-29483) (#1044)
Browse files Browse the repository at this point in the history
  • Loading branch information
rthalley authored Feb 9, 2024
1 parent f9a7c3f commit f66e25b
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 54 deletions.
45 changes: 31 additions & 14 deletions dns/asyncquery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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(
Expand All @@ -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.
Expand Down Expand Up @@ -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

Expand All @@ -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.
Expand Down Expand Up @@ -260,6 +276,7 @@ async def udp_with_fallback(
True,
udp_sock,
backend,
ignore_errors,
)
return (response, False)
except dns.message.Truncated:
Expand Down
2 changes: 2 additions & 0 deletions dns/nameserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ def query(
raise_on_truncation=True,
one_rr_per_rrset=one_rr_per_rrset,
ignore_trailing=ignore_trailing,
ignore_errors=True,
)
return response

Expand Down Expand Up @@ -153,6 +154,7 @@ async def async_query(
backend=backend,
one_rr_per_rrset=one_rr_per_rrset,
ignore_trailing=ignore_trailing,
ignore_errors=True,
)
return response

Expand Down
110 changes: 70 additions & 40 deletions dns/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand All @@ -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.
Expand Down Expand Up @@ -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``.
"""

Expand All @@ -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 (
Expand All @@ -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(
Expand All @@ -783,6 +812,7 @@ def udp_with_fallback(
ignore_trailing,
True,
udp_sock,
ignore_errors,
)
return (response, False)
except dns.message.Truncated:
Expand Down

0 comments on commit f66e25b

Please sign in to comment.