Add timeout for udp dns requests to avoid resource leak (#41395)
The tunneled UDP DNS request could leak if the Windows side produces no response. This PR adds a 60s timeout for each request to avoid this potential leak.
Feng Wang committed
Aug 21, 2026 at 10:26 UTC
efa237421c17bcbb49239d838d2250ab53d8b4a3
2 files changed
+59
-7
src/linux/init/DnsServer.cpp
+43
-6
@@ -4,8 +4,8 @@
4
#include <sys/epoll.h>
5
#include <netinet/in.h>
6
#include <sys/socket.h>
7
-#include "DnsServer.h"
7
#include "RuntimeErrorWithSourceLocation.h"
8
+#include "DnsServer.h"
9
#include "Syscall.h"
10
#include "util.h"
11
@@ -15,6 +15,8 @@ constexpr int c_dnsServerPort = 53;
15
constexpr int c_epollWaitMaxEvents = 100;
16
// Maximum size of DNS over UDP requests is 4096 bytes (max size is reached for EDNS UDP requests)
17
constexpr int c_maxUdpDnsBufferSize = 4096;
18
+// Maximum time to wait for a tunneled UDP DNS response
19
+constexpr auto c_udpRequestTimeout = std::chrono::seconds{60};
20
// Max number of pending connections in the TCP listen queue
21
constexpr int c_maxListenBacklog = 1000;
22
@@ -119,9 +121,12 @@ try
121
}
122
123
// Stop tracking the request, irrespective of the DNS response being successfully sent
122
- const auto removeDnsRequest = wil::scope_exit([&] { m_udpRequests.erase(dnsClientIdentifier.DnsClientId); });
124
+ const auto removeDnsRequest = wil::scope_exit([&] {
125
+ m_udpRequestExpirations.erase(it->second.m_expiration);
126
+ m_udpRequests.erase(it);
127
+ });
128
124
- sockaddr_in& remoteAddr = it->second;
129
+ sockaddr_in& remoteAddr = it->second.m_remoteAddress;
130
131
// Send DNS response buffer back to the Linux DNS client
132
int bufferSize = dnsBuffer.size();
@@ -298,6 +303,25 @@ try
303
}
304
CATCH_LOG()
305
306
+int DnsServer::ExpireUdpRequestsAndGetTimeout() noexcept
307
+{
308
+ std::scoped_lock<std::mutex> lock{m_udpLock};
309
+ const auto now = std::chrono::steady_clock::now();
310
+
311
+ while (!m_udpRequestExpirations.empty() && m_udpRequestExpirations.front().first <= now)
312
+ {
313
+ m_udpRequests.erase(m_udpRequestExpirations.front().second);
314
+ m_udpRequestExpirations.pop_front();
315
+ }
316
+
317
+ if (m_udpRequestExpirations.empty())
318
+ {
319
+ return -1;
320
+ }
321
+
322
+ return static_cast<int>(std::chrono::ceil<std::chrono::milliseconds>(m_udpRequestExpirations.front().first - now).count());
323
+}
324
+
325
void DnsServer::ServerLoop() noexcept
326
{
327
UtilSetThreadName("DnsServer");
@@ -311,7 +335,8 @@ void DnsServer::ServerLoop() noexcept
335
{
336
// A fixed number of events is requested from epoll_wait (c_epollWaitMaxEvents). In case the number of ready events is
337
// greater than c_epollWaitMaxEvents, epoll will round-robin through the ready events until we get a notification for all of them.
314
- size_t numReadyEvents = Syscall(epoll_wait, m_epollFd.get(), events, c_epollWaitMaxEvents, -1);
338
+ const auto timeout = ExpireUdpRequestsAndGetTimeout();
339
+ size_t numReadyEvents = Syscall(epoll_wait, m_epollFd.get(), events, c_epollWaitMaxEvents, timeout);
340
341
// No event
342
if (numReadyEvents == 0)
@@ -388,14 +413,26 @@ try
413
udpRequestId = requestId;
414
415
// Track the request
391
- m_udpRequests.emplace(requestId, remoteAddr);
416
+ const auto expiration = std::chrono::steady_clock::now() + c_udpRequestTimeout;
417
+ const auto expirationIt = m_udpRequestExpirations.emplace(m_udpRequestExpirations.end(), expiration, requestId);
418
+ auto removeExpirationOnError = wil::scope_exit([&] { m_udpRequestExpirations.erase(expirationIt); });
419
+
420
+ const auto [_, inserted] = m_udpRequests.emplace(requestId, UdpRequestContext{remoteAddr, expirationIt});
421
+ THROW_UNEXPECTED_IF(!inserted);
422
+
423
+ removeExpirationOnError.release();
424
}
425
426
if (!dnsRequest.empty())
427
{
428
auto removeRequestOnError = wil::scope_exit([&] {
429
std::scoped_lock<std::mutex> lock{m_udpLock};
398
- m_udpRequests.erase(udpRequestId);
430
+ const auto it = m_udpRequests.find(udpRequestId);
431
+ if (it != m_udpRequests.end())
432
+ {
433
+ m_udpRequestExpirations.erase(it->second.m_expiration);
434
+ m_udpRequests.erase(it);
435
+ }
436
});
437
438
// Tunnel request to Windows
src/linux/init/DnsServer.h
+16
-1
@@ -2,6 +2,8 @@
2
3
#pragma once
4
5
+#include <chrono>
6
+#include <list>
7
#include <map>
8
#include "common.h"
9
#include "lxinitshared.h"
@@ -68,6 +70,14 @@ private:
70
TcpConnectionContext& operator=(TcpConnectionContext&&) = delete;
71
};
72
73
+ using UdpRequestExpirationQueue = std::list<std::pair<std::chrono::steady_clock::time_point, uint32_t>>;
74
+
75
+ struct UdpRequestContext
76
+ {
77
+ sockaddr_in m_remoteAddress;
78
+ UdpRequestExpirationQueue::iterator m_expiration;
79
+ };
80
+
81
void StartUdpDnsServer(const std::string& ipAddress) noexcept;
82
83
void StartTcpDnsServer(const std::string& ipAddress) noexcept;
@@ -84,6 +94,8 @@ private:
94
// Read the next DNS request from the UDP socket.
95
void HandleUdpDnsRequest() noexcept;
96
97
+ int ExpireUdpRequestsAndGetTimeout() noexcept;
98
+
99
void HandleUdpDnsResponse(const gsl::span<gsl::byte> dnsBuffer, const LX_GNS_DNS_CLIENT_IDENTIFIER& dnsClientIdentifier) noexcept;
100
101
void HandleTcpDnsResponse(const gsl::span<gsl::byte> dnsBuffer, const LX_GNS_DNS_CLIENT_IDENTIFIER& dnsClientIdentifier) noexcept;
@@ -105,7 +117,10 @@ private:
117
// Mapping id of an UDP DNS request to the sockaddr_in struct storing the IP and port used by the Linux DNS client that made
118
// the DNS request. Note: Since we only configure an IPv4 DNS server in Linux, we expect all Linux DNS clients to use IPv4
119
// addresses. _Guarded_by_(m_udpLock)
108
- std::map<uint32_t, sockaddr_in> m_udpRequests;
120
+ std::map<uint32_t, UdpRequestContext> m_udpRequests;
121
+
122
+ // UDP requests ordered by expiration time. _Guarded_by_(m_udpLock)
123
+ UdpRequestExpirationQueue m_udpRequestExpirations;
124
125
wil::unique_fd m_tcpListenSocket;
126