VYPR
Moderate severityNVD Advisory· Published Apr 11, 2024· Updated Nov 4, 2025

CVE-2023-29483

CVE-2023-29483

Description

eventlet before 0.35.2, as used in dnspython before 2.6.0, allows remote attackers to interfere with DNS name resolution by quickly sending an invalid packet from the expected IP address and source port, aka a "TuDoor" attack. In other words, dnspython does not have the preferred behavior in which the DNS name resolution algorithm would proceed, within the full time window, in order to wait for a valid packet. NOTE: dnspython 2.6.0 is unusable for a different reason that was addressed in 2.6.1.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

CVE-2023-29483, aka 'TuDoor', allows remote attackers to disrupt DNS resolution by sending an invalid packet from the expected IP and port, affecting dnspython before 2.6.0 and eventlet before 0.35.2.

Vulnerability

Description

The TuDoor vulnerability (CVE-2023-29483) is a flaw in the DNS name resolution algorithm when using eventlet before 0.35.2 and dnspython before 2.6.0. An attacker can interfere with DNS resolution by quickly sending an invalid packet from the expected IP address and source port, causing the resolver to accept the spoofed response and potentially fail over to another resolver or give up entirely [1][2].

Exploitation

The attack requires the attacker to know the source IP and port used by the resolver for a specific query. By sending a crafted invalid packet before the legitimate response arrives, the resolver processes the spoofed packet and may discard the valid response. No authentication is required, and the attacker can be remote as long as they can spoof packets from the resolver's expected peer [1][3].

Impact

Successful exploitation can lead to denial of service for DNS resolution, as the stub resolver may switch to a different resolver or timeout, preventing legitimate DNS queries from being answered. This could impact any application relying on dnspython or eventlet for DNS, potentially causing network disruptions [2].

Mitigation

The issue is fixed in dnspython 2.6.1 (note: 2.6.0 is unusable due to a separate bug that also truncated legitimate responses) and eventlet 0.35.2. Users should upgrade to these versions or later. Additionally, eventlet is now discouraged for new projects, and migration to asyncio is recommended [4].

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
eventletPyPI
< 0.35.20.35.2
dnspythonPyPI
< 2.6.12.6.1

Affected products

48

Patches

2
51e3c4928d49

Dnspython 2.6.1 - Address DoS via the Tudoor mechanism (CVE-2023-29483)

https://github.com/eventlet/eventletKelvin J LiFeb 19, 2024via ghsa
1 file changed · +38 18
  • eventlet/support/greendns.py+38 18 modified
    @@ -713,7 +713,7 @@ def _net_write(sock, data, expiration):
     def udp(q, where, timeout=DNS_QUERY_TIMEOUT, port=53,
             af=None, source=None, source_port=0, ignore_unexpected=False,
             one_rr_per_rrset=False, ignore_trailing=False,
    -        raise_on_truncation=False, sock=None):
    +        raise_on_truncation=False, sock=None, ignore_errors=False):
         """coro friendly replacement for dns.query.udp
         Return the response obtained after sending a query via UDP.
     
    @@ -752,7 +752,10 @@ def udp(q, where, timeout=DNS_QUERY_TIMEOUT, port=53,
         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.
    -    @type sock: socket.socket | None"""
    +    @type sock: socket.socket | None
    +    @param ignore_errors: if various format errors or response mismatches occur,
    +    continue listening.
    +    @type ignore_errors: bool"""
     
         wire = q.to_wire()
         if af is None:
    @@ -816,26 +819,43 @@ def udp(q, where, timeout=DNS_QUERY_TIMEOUT, port=53,
                     addr = from_address[0]
                     addr = dns.ipv6.inet_ntoa(dns.ipv6.inet_aton(addr))
                     from_address = (addr, from_address[1], from_address[2], from_address[3])
    -            if from_address == destination:
    +            if from_address != destination:
    +                if ignore_unexpected:
    +                    continue
    +                else:
    +                    raise dns.query.UnexpectedSource(
    +                        'got a response from %s instead of %s'
    +                        % (from_address, destination))
    +            try:
    +                if _handle_raise_on_truncation:
    +                    r = dns.message.from_wire(wire,
    +                                              keyring=q.keyring,
    +                                              request_mac=q.mac,
    +                                              one_rr_per_rrset=one_rr_per_rrset,
    +                                              ignore_trailing=ignore_trailing,
    +                                              raise_on_truncation=raise_on_truncation)
    +                else:
    +                    r = dns.message.from_wire(wire,
    +                                              keyring=q.keyring,
    +                                              request_mac=q.mac,
    +                                              one_rr_per_rrset=one_rr_per_rrset,
    +                                              ignore_trailing=ignore_trailing)
    +                if not q.is_response(r):
    +                    raise dns.query.BadResponse()
                     break
    -            if not ignore_unexpected:
    -                raise dns.query.UnexpectedSource(
    -                    'got a response from %s instead of %s'
    -                    % (from_address, destination))
    +            except dns.message.Truncated as e:
    +                if ignore_errors and not q.is_response(e.message()):
    +                    continue
    +                else:
    +                    raise
    +            except Exception:
    +                if ignore_errors:
    +                    continue
    +                else:
    +                    raise
         finally:
             s.close()
     
    -    if _handle_raise_on_truncation:
    -        r = dns.message.from_wire(wire, keyring=q.keyring, request_mac=q.mac,
    -                                  one_rr_per_rrset=one_rr_per_rrset,
    -                                  ignore_trailing=ignore_trailing,
    -                                  raise_on_truncation=raise_on_truncation)
    -    else:
    -        r = dns.message.from_wire(wire, keyring=q.keyring, request_mac=q.mac,
    -                                  one_rr_per_rrset=one_rr_per_rrset,
    -                                  ignore_trailing=ignore_trailing)
    -    if not q.is_response(r):
    -        raise dns.query.BadResponse()
         return r
     
     
    
0ea5ad0a4583

The Tudoor fix should not eat valid Truncated exceptions [#1053] (#1054)

https://github.com/rthalley/dnspythonBob HalleyFeb 18, 2024via ghsa
4 files changed · +126 2
  • dns/asyncquery.py+10 0 modified
    @@ -151,6 +151,16 @@ async def receive_udp(
                     ignore_trailing=ignore_trailing,
                     raise_on_truncation=raise_on_truncation,
                 )
    +        except dns.message.Truncated as e:
    +            # See the comment in query.py for details.
    +            if (
    +                ignore_errors
    +                and query is not None
    +                and not query.is_response(e.message())
    +            ):
    +                continue
    +            else:
    +                raise
             except Exception:
                 if ignore_errors:
                     continue
    
  • dns/query.py+14 0 modified
    @@ -638,6 +638,20 @@ def receive_udp(
                     ignore_trailing=ignore_trailing,
                     raise_on_truncation=raise_on_truncation,
                 )
    +        except dns.message.Truncated as e:
    +            # If we got Truncated and not FORMERR, we at least got the header with TC
    +            # set, and very likely the question section, so we'll re-raise if the
    +            # message seems to be a response as we need to know when truncation happens.
    +            # We need to check that it seems to be a response as we don't want a random
    +            # injected message with TC set to cause us to bail out.
    +            if (
    +                ignore_errors
    +                and query is not None
    +                and not query.is_response(e.message())
    +            ):
    +                continue
    +            else:
    +                raise
             except Exception:
                 if ignore_errors:
                     continue
    
  • tests/test_async.py+59 1 modified
    @@ -705,17 +705,22 @@ async def mock_receive(
             from2,
             ignore_unexpected=True,
             ignore_errors=True,
    +        raise_on_truncation=False,
    +        good_r=None,
         ):
    +        if good_r is None:
    +            good_r = self.good_r
             s = MockSock(wire1, from1, wire2, from2)
             (r, when, _) = await dns.asyncquery.receive_udp(
                 s,
                 ("127.0.0.1", 53),
                 time.time() + 2,
                 ignore_unexpected=ignore_unexpected,
                 ignore_errors=ignore_errors,
    +            raise_on_truncation=raise_on_truncation,
                 query=self.q,
             )
    -        self.assertEqual(r, self.good_r)
    +        self.assertEqual(r, good_r)
     
         def test_good_mock(self):
             async def run():
    @@ -802,6 +807,59 @@ async def run():
     
             self.async_run(run)
     
    +    def test_good_wire_with_truncation_flag_and_no_truncation_raise(self):
    +        async def run():
    +            tc_r = dns.message.make_response(self.q)
    +            tc_r.flags |= dns.flags.TC
    +            tc_r_wire = tc_r.to_wire()
    +            await self.mock_receive(
    +                tc_r_wire, ("127.0.0.1", 53), None, None, good_r=tc_r
    +            )
    +
    +        self.async_run(run)
    +
    +    def test_good_wire_with_truncation_flag_and_truncation_raise(self):
    +        async def agood():
    +            tc_r = dns.message.make_response(self.q)
    +            tc_r.flags |= dns.flags.TC
    +            tc_r_wire = tc_r.to_wire()
    +            await self.mock_receive(
    +                tc_r_wire, ("127.0.0.1", 53), None, None, raise_on_truncation=True
    +            )
    +
    +        def good():
    +            self.async_run(agood)
    +
    +        self.assertRaises(dns.message.Truncated, good)
    +
    +    def test_wrong_id_wire_with_truncation_flag_and_no_truncation_raise(self):
    +        async def run():
    +            bad_r = dns.message.make_response(self.q)
    +            bad_r.id += 1
    +            bad_r.flags |= dns.flags.TC
    +            bad_r_wire = bad_r.to_wire()
    +            await self.mock_receive(
    +                bad_r_wire, ("127.0.0.1", 53), self.good_r_wire, ("127.0.0.1", 53)
    +            )
    +
    +        self.async_run(run)
    +
    +    def test_wrong_id_wire_with_truncation_flag_and_truncation_raise(self):
    +        async def run():
    +            bad_r = dns.message.make_response(self.q)
    +            bad_r.id += 1
    +            bad_r.flags |= dns.flags.TC
    +            bad_r_wire = bad_r.to_wire()
    +            await self.mock_receive(
    +                bad_r_wire,
    +                ("127.0.0.1", 53),
    +                self.good_r_wire,
    +                ("127.0.0.1", 53),
    +                raise_on_truncation=True,
    +            )
    +
    +        self.async_run(run)
    +
         def test_bad_wire_not_ignored(self):
             bad_r = dns.message.make_response(self.q)
             bad_r.id += 1
    
  • tests/test_query.py+43 1 modified
    @@ -29,6 +29,7 @@
         have_ssl = False
     
     import dns.exception
    +import dns.flags
     import dns.inet
     import dns.message
     import dns.name
    @@ -706,7 +707,11 @@ def mock_receive(
             from2,
             ignore_unexpected=True,
             ignore_errors=True,
    +        raise_on_truncation=False,
    +        good_r=None,
         ):
    +        if good_r is None:
    +            good_r = self.good_r
             s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
             try:
                 with mock_udp_recv(wire1, from1, wire2, from2):
    @@ -716,9 +721,10 @@ def mock_receive(
                         time.time() + 2,
                         ignore_unexpected=ignore_unexpected,
                         ignore_errors=ignore_errors,
    +                    raise_on_truncation=raise_on_truncation,
                         query=self.q,
                     )
    -                self.assertEqual(r, self.good_r)
    +                self.assertEqual(r, good_r)
             finally:
                 s.close()
     
    @@ -787,6 +793,42 @@ def test_bad_wire(self):
                 bad_r_wire[:10], ("127.0.0.1", 53), self.good_r_wire, ("127.0.0.1", 53)
             )
     
    +    def test_good_wire_with_truncation_flag_and_no_truncation_raise(self):
    +        tc_r = dns.message.make_response(self.q)
    +        tc_r.flags |= dns.flags.TC
    +        tc_r_wire = tc_r.to_wire()
    +        self.mock_receive(tc_r_wire, ("127.0.0.1", 53), None, None, good_r=tc_r)
    +
    +    def test_good_wire_with_truncation_flag_and_truncation_raise(self):
    +        def good():
    +            tc_r = dns.message.make_response(self.q)
    +            tc_r.flags |= dns.flags.TC
    +            tc_r_wire = tc_r.to_wire()
    +            self.mock_receive(
    +                tc_r_wire, ("127.0.0.1", 53), None, None, raise_on_truncation=True
    +            )
    +
    +        self.assertRaises(dns.message.Truncated, good)
    +
    +    def test_wrong_id_wire_with_truncation_flag_and_no_truncation_raise(self):
    +        bad_r = dns.message.make_response(self.q)
    +        bad_r.id += 1
    +        bad_r.flags |= dns.flags.TC
    +        bad_r_wire = bad_r.to_wire()
    +        self.mock_receive(
    +            bad_r_wire, ("127.0.0.1", 53), self.good_r_wire, ("127.0.0.1", 53)
    +        )
    +
    +    def test_wrong_id_wire_with_truncation_flag_and_truncation_raise(self):
    +        bad_r = dns.message.make_response(self.q)
    +        bad_r.id += 1
    +        bad_r.flags |= dns.flags.TC
    +        bad_r_wire = bad_r.to_wire()
    +        self.mock_receive(
    +            bad_r_wire, ("127.0.0.1", 53), self.good_r_wire, ("127.0.0.1", 53),
    +            raise_on_truncation=True
    +        )
    +
         def test_bad_wire_not_ignored(self):
             bad_r = dns.message.make_response(self.q)
             bad_r.id += 1
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

19

News mentions

0

No linked articles in our index yet.