[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Bug#1106210: unblock: dnsdist/1.9.10-1 [pre-approval, security]



Package: release.debian.org
Severity: normal
User: release.debian.org@packages.debian.org
Usertags: unblock
X-Debbugs-Cc: dnsdist@packages.debian.org, team@security.debian.org
Control: affects -1 + src:dnsdist

Please unblock package dnsdist

[ Reason ]
New upstream bugfix release with fix for security issue CVE-2025-30193 #1106207

I've picked the complete upstream minor release instead of 
cherry-picking the single fix, as the remaining diff is small. In 
addition to the CVE fix, we get: 1) fix for newer systemd versions / 
socket-family sandboxing, 2) fix for newer prometheus scrapers,
3) tiny feature to get the incoming network interface in Lua 
scripting.

[ Impact ]
CVE-2025-30193 will be unfixed if not uploaded.

[ Tests ]
I've reviewed the diff, did a test build and did a runtime test on a 
very small setup.

[ Risks ]
IMO the security fix is the large part of the diff, the rest seems 
trivial to me.

[ Checklist ]
  [x] all changes are documented in the d/changelog
  [x] I reviewed all changes and I approve them
  [x] attach debdiff against the package in testing

[ Other info ]
Nothing I'm aware of.

unblock dnsdist/1.9.10-1
diff -Nru dnsdist-1.9.9/configure dnsdist-1.9.10/configure
--- dnsdist-1.9.9/configure	2025-04-29 11:46:28.000000000 +0200
+++ dnsdist-1.9.10/configure	2025-05-20 11:13:44.000000000 +0200
@@ -1,6 +1,6 @@
 #! /bin/sh
 # Guess values for system-dependent variables and create Makefiles.
-# Generated by GNU Autoconf 2.71 for dnsdist 1.9.9.
+# Generated by GNU Autoconf 2.71 for dnsdist 1.9.10.
 #
 #
 # Copyright (C) 1992-1996, 1998-2017, 2020-2021 Free Software Foundation,
@@ -618,8 +618,8 @@
 # Identity of this package.
 PACKAGE_NAME='dnsdist'
 PACKAGE_TARNAME='dnsdist'
-PACKAGE_VERSION='1.9.9'
-PACKAGE_STRING='dnsdist 1.9.9'
+PACKAGE_VERSION='1.9.10'
+PACKAGE_STRING='dnsdist 1.9.10'
 PACKAGE_BUGREPORT=''
 PACKAGE_URL=''
 
@@ -1645,7 +1645,7 @@
   # Omit some internal or obsolete options to make the list less imposing.
   # This message is too long to be a string in the A/UX 3.1 sh.
   cat <<_ACEOF
-\`configure' configures dnsdist 1.9.9 to adapt to many kinds of systems.
+\`configure' configures dnsdist 1.9.10 to adapt to many kinds of systems.
 
 Usage: $0 [OPTION]... [VAR=VALUE]...
 
@@ -1716,7 +1716,7 @@
 
 if test -n "$ac_init_help"; then
   case $ac_init_help in
-     short | recursive ) echo "Configuration of dnsdist 1.9.9:";;
+     short | recursive ) echo "Configuration of dnsdist 1.9.10:";;
    esac
   cat <<\_ACEOF
 
@@ -1951,7 +1951,7 @@
 test -n "$ac_init_help" && exit $ac_status
 if $ac_init_version; then
   cat <<\_ACEOF
-dnsdist configure 1.9.9
+dnsdist configure 1.9.10
 generated by GNU Autoconf 2.71
 
 Copyright (C) 2021 Free Software Foundation, Inc.
@@ -2440,7 +2440,7 @@
 This file contains any messages produced by compilers while
 running configure, to aid debugging if configure makes a mistake.
 
-It was created by dnsdist $as_me 1.9.9, which was
+It was created by dnsdist $as_me 1.9.10, which was
 generated by GNU Autoconf 2.71.  Invocation command line was
 
   $ $0$ac_configure_args_raw
@@ -3932,7 +3932,7 @@
 
 # Define the identity of the package.
  PACKAGE='dnsdist'
- VERSION='1.9.9'
+ VERSION='1.9.10'
 
 
 printf "%s\n" "#define PACKAGE \"$PACKAGE\"" >>confdefs.h
@@ -28627,7 +28627,7 @@
 # report actual input values of CONFIG_FILES etc. instead of their
 # values after options handling.
 ac_log="
-This file was extended by dnsdist $as_me 1.9.9, which was
+This file was extended by dnsdist $as_me 1.9.10, which was
 generated by GNU Autoconf 2.71.  Invocation command line was
 
   CONFIG_FILES    = $CONFIG_FILES
@@ -28695,7 +28695,7 @@
 cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
 ac_cs_config='$ac_cs_config_escaped'
 ac_cs_version="\\
-dnsdist config.status 1.9.9
+dnsdist config.status 1.9.10
 configured by $0, generated by GNU Autoconf 2.71,
   with options \\"\$ac_cs_config\\"
 
diff -Nru dnsdist-1.9.9/configure.ac dnsdist-1.9.10/configure.ac
--- dnsdist-1.9.9/configure.ac	2025-04-29 11:46:19.000000000 +0200
+++ dnsdist-1.9.10/configure.ac	2025-05-20 11:13:35.000000000 +0200
@@ -1,6 +1,6 @@
 AC_PREREQ([2.69])
 
-AC_INIT([dnsdist], [1.9.9])
+AC_INIT([dnsdist], [1.9.10])
 AM_INIT_AUTOMAKE([foreign tar-ustar dist-bzip2 no-dist-gzip parallel-tests 1.11 subdir-objects])
 AM_SILENT_RULES([yes])
 AC_CONFIG_MACRO_DIR([m4])
diff -Nru dnsdist-1.9.9/credentials.hh dnsdist-1.9.10/credentials.hh
--- dnsdist-1.9.9/credentials.hh	2025-04-29 11:46:03.000000000 +0200
+++ dnsdist-1.9.10/credentials.hh	2025-05-20 11:13:25.000000000 +0200
@@ -21,7 +21,7 @@
  */
 #pragma once
 
-#include <memory>
+#include <cstdint>
 #include <string>
 
 class SensitiveData
diff -Nru dnsdist-1.9.9/debian/changelog dnsdist-1.9.10/debian/changelog
--- dnsdist-1.9.9/debian/changelog	2025-04-29 14:27:45.000000000 +0200
+++ dnsdist-1.9.10/debian/changelog	2025-05-21 10:30:17.000000000 +0200
@@ -1,3 +1,10 @@
+dnsdist (1.9.10-1) unstable; urgency=medium
+
+  * New upstream version 1.9.10 including fix for CVE-2025-30193
+    (Closes: #1106207)
+
+ -- Chris Hofstaedtler <zeha@debian.org>  Wed, 21 May 2025 10:30:17 +0200
+
 dnsdist (1.9.9-1) unstable; urgency=medium
 
   * New upstream version 1.9.9 including fix for CVE-2025-30194
diff -Nru dnsdist-1.9.9/dnsdist.1 dnsdist-1.9.10/dnsdist.1
--- dnsdist-1.9.9/dnsdist.1	2025-04-29 11:47:05.000000000 +0200
+++ dnsdist-1.9.10/dnsdist.1	2025-05-20 11:14:13.000000000 +0200
@@ -27,7 +27,7 @@
 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
 ..
-.TH "DNSDIST" "1" "Apr 29, 2025" "" "dnsdist"
+.TH "DNSDIST" "1" "May 20, 2025" "" "dnsdist"
 .SH NAME
 dnsdist \- A DNS and DoS aware, scriptable loadbalancer
 .SH SYNOPSIS
diff -Nru dnsdist-1.9.9/dnsdist-backend.cc dnsdist-1.9.10/dnsdist-backend.cc
--- dnsdist-1.9.9/dnsdist-backend.cc	2025-04-29 11:46:04.000000000 +0200
+++ dnsdist-1.9.10/dnsdist-backend.cc	2025-05-20 11:13:25.000000000 +0200
@@ -144,7 +144,12 @@
     }
     catch (const std::runtime_error& error) {
       if (initialAttempt || g_verbose) {
-        infolog("Error connecting to new server with address %s: %s", d_config.remote.toStringWithPort(), error.what());
+        if (!IsAnyAddress(d_config.sourceAddr) || !d_config.sourceItfName.empty()) {
+          infolog("Error connecting to new server with address %s (source address: %s, source interface: %s): %s", d_config.remote.toStringWithPort(), IsAnyAddress(d_config.sourceAddr) ? "not set" : d_config.sourceAddr.toString(), d_config.sourceItfName.empty() ? "not set" : d_config.sourceItfName, error.what());
+        }
+        else {
+          infolog("Error connecting to new server with address %s: %s", d_config.remote.toStringWithPort(), error.what());
+        }
       }
       connected = false;
       break;
@@ -974,6 +979,13 @@
     serv.first = idx++;
   }
   *servers = std::make_shared<const ServerPolicy::NumberedServerVector>(std::move(newServers));
+
+  if ((*servers)->size() == 1) {
+    d_tcpOnly = server->isTCPOnly();
+  }
+  else if (!server->isTCPOnly()) {
+    d_tcpOnly = false;
+  }
 }
 
 void ServerPool::removeServer(shared_ptr<DownstreamState>& server)
@@ -984,8 +996,10 @@
   auto newServers = std::make_shared<ServerPolicy::NumberedServerVector>(*(*servers));
   size_t idx = 1;
   bool found = false;
+  bool tcpOnly = true;
   for (auto it = newServers->begin(); it != newServers->end();) {
     if (found) {
+      tcpOnly = tcpOnly && it->second->isTCPOnly();
       /* we need to renumber the servers placed
          after the removed one, for Lua (custom policies) */
       it->first = idx++;
@@ -995,9 +1009,11 @@
       it = newServers->erase(it);
       found = true;
     } else {
+      tcpOnly = tcpOnly && it->second->isTCPOnly();
       idx++;
       it++;
     }
   }
+  d_tcpOnly = tcpOnly;
   *servers = std::move(newServers);
 }
diff -Nru dnsdist-1.9.9/dnsdist.cc dnsdist-1.9.10/dnsdist.cc
--- dnsdist-1.9.9/dnsdist.cc	2025-04-29 11:46:04.000000000 +0200
+++ dnsdist-1.9.10/dnsdist.cc	2025-05-20 11:13:25.000000000 +0200
@@ -1447,6 +1447,9 @@
     if (selectedBackend && selectedBackend->isTCPOnly()) {
       willBeForwardedOverUDP = false;
     }
+    else if (!selectedBackend) {
+      willBeForwardedOverUDP = !serverPool->isTCPOnly();
+    }
 
     uint32_t allowExpired = selectedBackend ? 0 : g_staleCacheEntriesTTL;
 
@@ -1693,9 +1696,13 @@
   bool doh = dnsQuestion.ids.du != nullptr;
 
   bool failed = false;
+  dnsQuestion.ids.d_proxyProtocolPayloadSize = 0;
   if (downstream->d_config.useProxyProtocol) {
     try {
-      addProxyProtocol(dnsQuestion, &dnsQuestion.ids.d_proxyProtocolPayloadSize);
+      size_t proxyProtocolPayloadSize = 0;
+      if (addProxyProtocol(dnsQuestion, &proxyProtocolPayloadSize)) {
+        dnsQuestion.ids.d_proxyProtocolPayloadSize += proxyProtocolPayloadSize;
+      }
     }
     catch (const std::exception& e) {
       vinfolog("Adding proxy protocol payload to %s query from %s failed: %s", (dnsQuestion.ids.du ? "DoH" : ""), dnsQuestion.ids.origDest.toStringWithPort(), e.what());
diff -Nru dnsdist-1.9.9/dnsdist.hh dnsdist-1.9.10/dnsdist.hh
--- dnsdist-1.9.9/dnsdist.hh	2025-04-29 11:46:04.000000000 +0200
+++ dnsdist-1.9.10/dnsdist.hh	2025-05-20 11:13:25.000000000 +0200
@@ -971,7 +971,7 @@
     return d_config.d_tcpOnly || d_config.d_tcpCheck || d_tlsCtx != nullptr;
   }
 
-  bool isTCPOnly() const
+  [[nodiscard]] bool isTCPOnly() const
   {
     return d_config.d_tcpOnly || d_tlsCtx != nullptr;
   }
@@ -1071,10 +1071,15 @@
   const std::shared_ptr<const ServerPolicy::NumberedServerVector> getServers();
   void addServer(shared_ptr<DownstreamState>& server);
   void removeServer(shared_ptr<DownstreamState>& server);
+   bool isTCPOnly() const
+  {
+    return d_tcpOnly;
+  }
 
 private:
   SharedLockGuarded<std::shared_ptr<const ServerPolicy::NumberedServerVector>> d_servers;
   bool d_useECS{false};
+  bool d_tcpOnly{false};
 };
 
 enum ednsHeaderFlags {
diff -Nru dnsdist-1.9.9/dnsdist-lua-bindings.cc dnsdist-1.9.10/dnsdist-lua-bindings.cc
--- dnsdist-1.9.9/dnsdist-lua-bindings.cc	2025-04-29 11:46:04.000000000 +0200
+++ dnsdist-1.9.10/dnsdist-lua-bindings.cc	2025-05-20 11:13:25.000000000 +0200
@@ -845,7 +845,7 @@
     if (client || configCheck) {
       return;
     }
-    std::thread newThread(dnsdist::resolver::asynchronousResolver, std::move(hostname), [callback=std::move(callback)](const std::string& resolvedHostname, std::vector<ComboAddress>& ips) {
+    std::thread newThread(dnsdist::resolver::asynchronousResolver, std::move(hostname), [callback = std::move(callback)](const std::string& resolvedHostname, std::vector<ComboAddress>& ips) mutable {
       LuaArray<ComboAddress> result;
       result.reserve(ips.size());
       for (const auto& entry : ips) {
@@ -853,7 +853,15 @@
       }
       {
         auto lua = g_lua.lock();
-        callback(resolvedHostname, result);
+        try {
+          callback(resolvedHostname, result);
+        }
+        catch (const std::exception& exp) {
+          vinfolog("Error during execution of getAddressInfo callback: %s", exp.what());
+        }
+        // this _needs_ to be done while we are holding the lock,
+        // otherwise the destructor will corrupt the stack
+        callback = nullptr;
         dnsdist::handleQueuedAsynchronousEvents();
       }
     });
diff -Nru dnsdist-1.9.9/dnsdist-lua-bindings-dnsquestion.cc dnsdist-1.9.10/dnsdist-lua-bindings-dnsquestion.cc
--- dnsdist-1.9.9/dnsdist-lua-bindings-dnsquestion.cc	2025-04-29 11:46:04.000000000 +0200
+++ dnsdist-1.9.10/dnsdist-lua-bindings-dnsquestion.cc	2025-05-20 11:13:25.000000000 +0200
@@ -138,6 +138,13 @@
       return dq.sni;
     });
 
+  luaCtx.registerFunction<std::string (DNSQuestion::*)() const>("getIncomingInterface", [](const DNSQuestion& dnsQuestion) -> std::string {
+    if (dnsQuestion.ids.cs != nullptr) {
+      return dnsQuestion.ids.cs->interface;
+    }
+    return {};
+  });
+
   luaCtx.registerFunction<std::string (DNSQuestion::*)()const>("getProtocol", [](const DNSQuestion& dq) {
     return dq.getProtocol().toPrettyString();
   });
@@ -458,6 +465,13 @@
     return dnsResponse.ids.queryRealTime.udiff();
   });
 
+  luaCtx.registerFunction<std::string (DNSResponse::*)() const>("getIncomingInterface", [](const DNSResponse& dnsResponse) -> std::string {
+    if (dnsResponse.ids.cs != nullptr) {
+      return dnsResponse.ids.cs->interface;
+    }
+    return {};
+  });
+
   luaCtx.registerFunction<void(DNSResponse::*)(std::string)>("sendTrap", [](const DNSResponse& dr, boost::optional<std::string> reason) {
 #ifdef HAVE_NET_SNMP
       if (g_snmpAgent && g_snmpTrapsEnabled) {
@@ -551,6 +565,7 @@
     }
     dr.asynchronous = true;
     dr.getMutableData() = *dr.ids.d_packet;
+    dr.ids.d_proxyProtocolPayloadSize = 0;
     auto query = dnsdist::getInternalQueryFromDQ(dr, false);
     return dnsdist::queueQueryResumptionEvent(std::move(query));
   });
diff -Nru dnsdist-1.9.9/dnsdist-lua.cc dnsdist-1.9.10/dnsdist-lua.cc
--- dnsdist-1.9.9/dnsdist-lua.cc	2025-04-29 11:46:04.000000000 +0200
+++ dnsdist-1.9.10/dnsdist-lua.cc	2025-05-20 11:13:25.000000000 +0200
@@ -642,7 +642,7 @@
                          auto ret = std::make_shared<DownstreamState>(std::move(config), std::move(tlsCtx), !(client || configCheck));
 #ifdef HAVE_XSK
                          LuaArray<std::shared_ptr<XskSocket>> luaXskSockets;
-                         if (getOptionalValue<LuaArray<std::shared_ptr<XskSocket>>>(vars, "xskSockets", luaXskSockets) > 0 && !luaXskSockets.empty()) {
+                         if (!client && !configCheck && getOptionalValue<LuaArray<std::shared_ptr<XskSocket>>>(vars, "xskSockets", luaXskSockets) > 0 && !luaXskSockets.empty()) {
                            if (g_configurationDone) {
                              throw std::runtime_error("Adding a server with xsk at runtime is not supported");
                            }
@@ -668,6 +668,13 @@
                          else if (!(client || configCheck)) {
                            infolog("Added downstream server %s", ret->d_config.remote.toStringWithPort());
                          }
+
+                         if (client || configCheck) {
+                           /* consume these in client or configuration check mode, to prevent warnings */
+                           std::string mac;
+                           getOptionalValue<std::string>(vars, "MACAddr", mac);
+                           getOptionalValue<LuaArray<std::shared_ptr<XskSocket>>>(vars, "xskSockets", luaXskSockets);
+                         }
 #else /* HAVE_XSK */
       if (!(client || configCheck)) {
         infolog("Added downstream server %s", ret->d_config.remote.toStringWithPort());
diff -Nru dnsdist-1.9.9/dnsdist-lua-ffi.cc dnsdist-1.9.10/dnsdist-lua-ffi.cc
--- dnsdist-1.9.9/dnsdist-lua-ffi.cc	2025-04-29 11:46:04.000000000 +0200
+++ dnsdist-1.9.10/dnsdist-lua-ffi.cc	2025-05-20 11:13:25.000000000 +0200
@@ -121,6 +121,14 @@
   return dq->dq->ids.origRemote.getPort();
 }
 
+const char* dnsdist_ffi_dnsquestion_get_incoming_interface(const dnsdist_ffi_dnsquestion_t* dnsQuestion)
+{
+  if (dnsQuestion == nullptr || dnsQuestion->dq == nullptr || dnsQuestion->dq->ids.cs == nullptr) {
+    return nullptr;
+  }
+  return dnsQuestion->dq->ids.cs->interface.c_str();
+}
+
 void dnsdist_ffi_dnsquestion_get_qname_raw(const dnsdist_ffi_dnsquestion_t* dq, const char** qname, size_t* qnameSize)
 {
   const auto& storage = dq->dq->ids.qname.getStorage();
diff -Nru dnsdist-1.9.9/dnsdist-lua-ffi-interface.h dnsdist-1.9.10/dnsdist-lua-ffi-interface.h
--- dnsdist-1.9.9/dnsdist-lua-ffi-interface.h	2025-04-29 11:46:04.000000000 +0200
+++ dnsdist-1.9.10/dnsdist-lua-ffi-interface.h	2025-05-20 11:13:25.000000000 +0200
@@ -64,6 +64,7 @@
 void dnsdist_ffi_dnsquestion_get_remoteaddr(const dnsdist_ffi_dnsquestion_t* dq, const void** addr, size_t* addrSize) __attribute__ ((visibility ("default")));
 void dnsdist_ffi_dnsquestion_get_masked_remoteaddr(dnsdist_ffi_dnsquestion_t* dq, const void** addr, size_t* addrSize, uint8_t bits) __attribute__ ((visibility ("default")));
 uint16_t dnsdist_ffi_dnsquestion_get_remote_port(const dnsdist_ffi_dnsquestion_t* dq) __attribute__ ((visibility ("default")));
+const char* dnsdist_ffi_dnsquestion_get_incoming_interface(const dnsdist_ffi_dnsquestion_t* dnsQuestion) __attribute__ ((visibility ("default")));
 void dnsdist_ffi_dnsquestion_get_qname_raw(const dnsdist_ffi_dnsquestion_t* dq, const char** qname, size_t* qnameSize) __attribute__ ((visibility ("default")));
 size_t dnsdist_ffi_dnsquestion_get_qname_hash(const dnsdist_ffi_dnsquestion_t* dq, size_t init) __attribute__ ((visibility ("default")));
 uint16_t dnsdist_ffi_dnsquestion_get_qtype(const dnsdist_ffi_dnsquestion_t* dq) __attribute__ ((visibility ("default")));
diff -Nru dnsdist-1.9.9/dnsdist-lua-ffi-interface.inc dnsdist-1.9.10/dnsdist-lua-ffi-interface.inc
--- dnsdist-1.9.9/dnsdist-lua-ffi-interface.inc	2025-04-29 11:46:38.000000000 +0200
+++ dnsdist-1.9.10/dnsdist-lua-ffi-interface.inc	2025-05-20 11:13:53.000000000 +0200
@@ -65,6 +65,7 @@
 void dnsdist_ffi_dnsquestion_get_remoteaddr(const dnsdist_ffi_dnsquestion_t* dq, const void** addr, size_t* addrSize) __attribute__ ((visibility ("default")));
 void dnsdist_ffi_dnsquestion_get_masked_remoteaddr(dnsdist_ffi_dnsquestion_t* dq, const void** addr, size_t* addrSize, uint8_t bits) __attribute__ ((visibility ("default")));
 uint16_t dnsdist_ffi_dnsquestion_get_remote_port(const dnsdist_ffi_dnsquestion_t* dq) __attribute__ ((visibility ("default")));
+const char* dnsdist_ffi_dnsquestion_get_incoming_interface(const dnsdist_ffi_dnsquestion_t* dnsQuestion) __attribute__ ((visibility ("default")));
 void dnsdist_ffi_dnsquestion_get_qname_raw(const dnsdist_ffi_dnsquestion_t* dq, const char** qname, size_t* qnameSize) __attribute__ ((visibility ("default")));
 size_t dnsdist_ffi_dnsquestion_get_qname_hash(const dnsdist_ffi_dnsquestion_t* dq, size_t init) __attribute__ ((visibility ("default")));
 uint16_t dnsdist_ffi_dnsquestion_get_qtype(const dnsdist_ffi_dnsquestion_t* dq) __attribute__ ((visibility ("default")));
diff -Nru dnsdist-1.9.9/dnsdist-proxy-protocol.cc dnsdist-1.9.10/dnsdist-proxy-protocol.cc
--- dnsdist-1.9.9/dnsdist-proxy-protocol.cc	2025-04-29 11:46:04.000000000 +0200
+++ dnsdist-1.9.10/dnsdist-proxy-protocol.cc	2025-05-20 11:13:25.000000000 +0200
@@ -42,14 +42,19 @@
   return addProxyProtocol(dq.getMutableData(), payload);
 }
 
-bool addProxyProtocol(DNSQuestion& dq, size_t* payloadSize)
+bool addProxyProtocol(DNSQuestion& dnsQuestion, size_t* proxyProtocolPayloadSize)
 {
-  auto payload = getProxyProtocolPayload(dq);
-  if (payloadSize != nullptr) {
-    *payloadSize = payload.size();
+  auto payload = getProxyProtocolPayload(dnsQuestion);
+  size_t payloadSize = payload.size();
+
+  if (!addProxyProtocol(dnsQuestion, payload)) {
+    return false;
   }
 
-  return addProxyProtocol(dq, payload);
+  if (proxyProtocolPayloadSize != nullptr) {
+    *proxyProtocolPayloadSize = payloadSize;
+  }
+  return true;
 }
 
 bool addProxyProtocol(PacketBuffer& buffer, const std::string& payload)
diff -Nru dnsdist-1.9.9/dnsdist-proxy-protocol.hh dnsdist-1.9.10/dnsdist-proxy-protocol.hh
--- dnsdist-1.9.9/dnsdist-proxy-protocol.hh	2025-04-29 11:46:04.000000000 +0200
+++ dnsdist-1.9.10/dnsdist-proxy-protocol.hh	2025-05-20 11:13:25.000000000 +0200
@@ -29,7 +29,7 @@
 
 std::string getProxyProtocolPayload(const DNSQuestion& dq);
 
-bool addProxyProtocol(DNSQuestion& dq, size_t* proxyProtocolPayloadSize = nullptr);
+bool addProxyProtocol(DNSQuestion& dnsQuestion, size_t* proxyProtocolPayloadSize = nullptr);
 bool addProxyProtocol(DNSQuestion& dq, const std::string& payload);
 bool addProxyProtocol(PacketBuffer& buffer, const std::string& payload);
 bool addProxyProtocol(PacketBuffer& buffer, bool tcp, const ComboAddress& source, const ComboAddress& destination, const std::vector<ProxyProtocolValue>& values);
diff -Nru dnsdist-1.9.9/dnsdist.service.in dnsdist-1.9.10/dnsdist.service.in
--- dnsdist-1.9.9/dnsdist.service.in	2025-04-29 11:46:04.000000000 +0200
+++ dnsdist-1.9.10/dnsdist.service.in	2025-05-20 11:13:25.000000000 +0200
@@ -44,7 +44,7 @@
 ProtectKernelModules=true
 ProtectKernelTunables=true
 ProtectSystem=full
-RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
+RestrictAddressFamilies=AF_INET AF_INET6 AF_NETLINK AF_UNIX AF_XDP
 RestrictNamespaces=true
 RestrictRealtime=true
 RestrictSUIDSGID=true
diff -Nru dnsdist-1.9.9/dnsdist-tcp.cc dnsdist-1.9.10/dnsdist-tcp.cc
--- dnsdist-1.9.9/dnsdist-tcp.cc	2025-04-29 11:46:04.000000000 +0200
+++ dnsdist-1.9.10/dnsdist-tcp.cc	2025-05-20 11:13:25.000000000 +0200
@@ -114,14 +114,46 @@
   return t_downstreamTCPConnectionsManager.clear();
 }
 
+static std::pair<std::shared_ptr<TCPConnectionToBackend>, bool> getOwnedDownstreamConnection(std::map<std::shared_ptr<DownstreamState>, std::deque<std::shared_ptr<TCPConnectionToBackend>>>& ownedConnectionsToBackend, const std::shared_ptr<DownstreamState>& backend, const std::unique_ptr<std::vector<ProxyProtocolValue>>& tlvs)
+{
+  bool tlvsMismatch = false;
+  auto connIt = ownedConnectionsToBackend.find(backend);
+  if (connIt == ownedConnectionsToBackend.end()) {
+    DEBUGLOG("no owned connection found for " << backend->getName());
+    return {nullptr, tlvsMismatch};
+  }
+
+  for (auto& conn : connIt->second) {
+    if (conn->canBeReused(true)) {
+      if (conn->matchesTLVs(tlvs)) {
+        DEBUGLOG("Got one owned connection accepting more for " << backend->getName());
+        conn->setReused();
+        ++backend->tcpReusedConnections;
+        return {conn, tlvsMismatch};
+      }
+      DEBUGLOG("Found one connection to " << backend->getName() << " but with different TLV values");
+      tlvsMismatch = true;
+    }
+    DEBUGLOG("not accepting more for " << backend->getName());
+  }
+
+  return {nullptr, tlvsMismatch};
+}
+
 std::shared_ptr<TCPConnectionToBackend> IncomingTCPConnectionState::getDownstreamConnection(std::shared_ptr<DownstreamState>& backend, const std::unique_ptr<std::vector<ProxyProtocolValue>>& tlvs, const struct timeval& now)
 {
-  auto downstream = getOwnedDownstreamConnection(backend, tlvs);
+  auto [downstream, tlvsMismatch] = getOwnedDownstreamConnection(d_ownedConnectionsToBackend, backend, tlvs);
 
   if (!downstream) {
+    if (backend->d_config.useProxyProtocol && tlvsMismatch) {
+      clearOwnedDownstreamConnections(backend);
+    }
+
     /* we don't have a connection to this backend owned yet, let's get one (it might not be a fresh one, though) */
     downstream = t_downstreamTCPConnectionsManager.getConnectionToDownstream(d_threadData.mplexer, backend, now, std::string());
-    if (backend->d_config.useProxyProtocol) {
+    // if we had an existing connection but the TLVs are different, they are likely unique per query so do not bother keeping the connection
+    // around
+    if (backend->d_config.useProxyProtocol && !tlvsMismatch) {
       registerOwnedDownstreamConnection(downstream);
     }
   }
@@ -272,29 +304,32 @@
   d_state = State::waitingForQuery;
 }
 
-std::shared_ptr<TCPConnectionToBackend> IncomingTCPConnectionState::getOwnedDownstreamConnection(const std::shared_ptr<DownstreamState>& backend, const std::unique_ptr<std::vector<ProxyProtocolValue>>& tlvs)
+void IncomingTCPConnectionState::registerOwnedDownstreamConnection(std::shared_ptr<TCPConnectionToBackend>& conn)
 {
-  auto connIt = d_ownedConnectionsToBackend.find(backend);
-  if (connIt == d_ownedConnectionsToBackend.end()) {
-    DEBUGLOG("no owned connection found for " << backend->getName());
-    return nullptr;
-  }
+  const auto& downstream = conn->getDS();
 
-  for (auto& conn : connIt->second) {
-    if (conn->canBeReused(true) && conn->matchesTLVs(tlvs)) {
-      DEBUGLOG("Got one owned connection accepting more for " << backend->getName());
-      conn->setReused();
-      return conn;
-    }
-    DEBUGLOG("not accepting more for " << backend->getName());
-  }
+  auto& queue = d_ownedConnectionsToBackend[downstream];
+  // how many proxy-protocol enabled connections do we want to keep around?
+  // - they are only usable for this incoming connection because of the proxy protocol header containing the source and destination addresses and ports
+  // - if we have TLV values, and they are unique per query, keeping these is useless
+  // - if there is no, or identical, TLV values, a single outgoing connection is enough if maxInFlight == 1, or if incoming maxInFlight == outgoing maxInFlight
+  // so it makes sense to keep a few of them around if incoming maxInFlight is greater than outgoing maxInFlight
 
-  return nullptr;
+  auto incomingMaxInFlightQueriesPerConn = d_ci.cs->d_maxInFlightQueriesPerConn;
+  incomingMaxInFlightQueriesPerConn = std::max(incomingMaxInFlightQueriesPerConn, static_cast<size_t>(1U));
+  auto outgoingMaxInFlightQueriesPerConn = downstream->d_config.d_maxInFlightQueriesPerConn;
+  outgoingMaxInFlightQueriesPerConn = std::max(outgoingMaxInFlightQueriesPerConn, static_cast<size_t>(1U));
+  size_t maxCachedOutgoingConnections = std::min(static_cast<size_t>(incomingMaxInFlightQueriesPerConn / outgoingMaxInFlightQueriesPerConn), static_cast<size_t>(5U));
+
+  queue.push_front(conn);
+  if (queue.size() > maxCachedOutgoingConnections) {
+    queue.pop_back();
+  }
 }
 
-void IncomingTCPConnectionState::registerOwnedDownstreamConnection(std::shared_ptr<TCPConnectionToBackend>& conn)
+void IncomingTCPConnectionState::clearOwnedDownstreamConnections(const std::shared_ptr<DownstreamState>& downstream)
 {
-  d_ownedConnectionsToBackend[conn->getDS()].push_front(conn);
+  d_ownedConnectionsToBackend.erase(downstream);
 }
 
 /* called when the buffer has been set and the rules have been processed, and only from handleIO (sometimes indirectly via handleQuery) */
@@ -1053,8 +1088,39 @@
   return false;
 }
 
+class HandlingIOGuard
+{
+public:
+  HandlingIOGuard(bool& handlingIO) :
+    d_handlingIO(handlingIO)
+  {
+  }
+  HandlingIOGuard(const HandlingIOGuard&) = delete;
+  HandlingIOGuard(HandlingIOGuard&&) = delete;
+  HandlingIOGuard& operator=(const HandlingIOGuard& rhs) = delete;
+  HandlingIOGuard& operator=(HandlingIOGuard&&) = delete;
+  ~HandlingIOGuard()
+  {
+    d_handlingIO = false;
+  }
+
+private:
+  bool& d_handlingIO;
+};
+
 void IncomingTCPConnectionState::handleIO()
 {
+  // let's make sure we are not already in handleIO() below in the stack:
+  // this might happen when we have a response available on the backend socket
+  // right after forwarding the query, and then a query waiting for us on the
+  // client socket right after forwarding the response, and then a response available
+  // on the backend socket right after forwarding the query.. you get the idea.
+  if (d_handlingIO) {
+    return;
+  }
+  d_handlingIO = true;
+  HandlingIOGuard reentryGuard(d_handlingIO);
+
   // why do we loop? Because the TLS layer does buffering, and thus can have data ready to read
   // even though the underlying socket is not ready, so we need to actually ask for the data first
   IOState iostate = IOState::Done;
diff -Nru dnsdist-1.9.9/dnsdist-tcp-upstream.hh dnsdist-1.9.10/dnsdist-tcp-upstream.hh
--- dnsdist-1.9.9/dnsdist-tcp-upstream.hh	2025-04-29 11:46:04.000000000 +0200
+++ dnsdist-1.9.10/dnsdist-tcp-upstream.hh	2025-05-20 11:13:25.000000000 +0200
@@ -116,9 +116,9 @@
     return false;
   }
 
-  std::shared_ptr<TCPConnectionToBackend> getOwnedDownstreamConnection(const std::shared_ptr<DownstreamState>& backend, const std::unique_ptr<std::vector<ProxyProtocolValue>>& tlvs);
   std::shared_ptr<TCPConnectionToBackend> getDownstreamConnection(std::shared_ptr<DownstreamState>& backend, const std::unique_ptr<std::vector<ProxyProtocolValue>>& tlvs, const struct timeval& now);
   void registerOwnedDownstreamConnection(std::shared_ptr<TCPConnectionToBackend>& conn);
+  void clearOwnedDownstreamConnections(const std::shared_ptr<DownstreamState>& downstream);
 
   static size_t clearAllDownstreamConnections();
 
@@ -216,4 +216,5 @@
   bool d_proxyProtocolPayloadHasTLV{false};
   bool d_lastIOBlocked{false};
   bool d_hadErrors{false};
+  bool d_handlingIO{false};
 };
diff -Nru dnsdist-1.9.9/dnsdist-web.cc dnsdist-1.9.10/dnsdist-web.cc
--- dnsdist-1.9.9/dnsdist-web.cc	2025-04-29 11:46:04.000000000 +0200
+++ dnsdist-1.9.10/dnsdist-web.cc	2025-05-20 11:13:25.000000000 +0200
@@ -913,7 +913,7 @@
   output << "dnsdist_info{version=\"" << VERSION << "\"} " << "1" << "\n";
 
   resp.body = output.str();
-  resp.headers["Content-Type"] = "text/plain";
+  resp.headers["Content-Type"] = "text/plain; version=0.0.4";
 }
 #endif /* DISABLE_PROMETHEUS */
 
diff -Nru dnsdist-1.9.9/doh3.cc dnsdist-1.9.10/doh3.cc
--- dnsdist-1.9.9/doh3.cc	2025-04-29 11:46:04.000000000 +0200
+++ dnsdist-1.9.10/doh3.cc	2025-05-20 11:13:25.000000000 +0200
@@ -912,14 +912,14 @@
       if (!quiche_version_is_supported(version)) {
         DEBUGLOG("Unsupported version");
         ++frontend.d_doh3UnsupportedVersionErrors;
-        handleVersionNegociation(sock, clientConnID, serverConnID, client, localAddr, buffer);
+        handleVersionNegotiation(sock, clientConnID, serverConnID, client, localAddr, buffer, clientState.local.isUnspecified());
         continue;
       }
 
       if (token_len == 0) {
         /* stateless retry */
         DEBUGLOG("No token received");
-        handleStatelessRetry(sock, clientConnID, serverConnID, client, localAddr, version, buffer);
+        handleStatelessRetry(sock, clientConnID, serverConnID, client, localAddr, version, buffer, clientState.local.isUnspecified());
         continue;
       }
 
@@ -966,7 +966,7 @@
 
       processH3Events(clientState, frontend, conn->get(), client, serverConnID, buffer);
 
-      flushEgress(sock, conn->get().d_conn, client, localAddr, buffer);
+      flushEgress(sock, conn->get().d_conn, client, localAddr, buffer, clientState.local.isUnspecified());
     }
     else {
       DEBUGLOG("Connection not established");
@@ -1011,7 +1011,7 @@
         for (auto conn = frontend->d_server_config->d_connections.begin(); conn != frontend->d_server_config->d_connections.end();) {
           quiche_conn_on_timeout(conn->second.d_conn.get());
 
-          flushEgress(sock, conn->second.d_conn, conn->second.d_peer, conn->second.d_localAddr, buffer);
+          flushEgress(sock, conn->second.d_conn, conn->second.d_peer, conn->second.d_localAddr, buffer, clientState->local.isUnspecified());
 
           if (quiche_conn_is_closed(conn->second.d_conn.get())) {
 #ifdef DEBUGLOG_ENABLED
diff -Nru dnsdist-1.9.9/doq.cc dnsdist-1.9.10/doq.cc
--- dnsdist-1.9.9/doq.cc	2025-04-29 11:46:04.000000000 +0200
+++ dnsdist-1.9.10/doq.cc	2025-05-20 11:13:25.000000000 +0200
@@ -718,14 +718,14 @@
       if (!quiche_version_is_supported(version)) {
         DEBUGLOG("Unsupported version");
         ++frontend.d_doqUnsupportedVersionErrors;
-        handleVersionNegociation(sock, clientConnID, serverConnID, client, localAddr, buffer);
+        handleVersionNegotiation(sock, clientConnID, serverConnID, client, localAddr, buffer, clientState.local.isUnspecified());
         continue;
       }
 
       if (token_len == 0) {
         /* stateless retry */
         DEBUGLOG("No token received");
-        handleStatelessRetry(sock, clientConnID, serverConnID, client, localAddr, version, buffer);
+        handleStatelessRetry(sock, clientConnID, serverConnID, client, localAddr, version, buffer, clientState.local.isUnspecified());
         continue;
       }
 
@@ -766,7 +766,7 @@
         handleReadableStream(frontend, clientState, *conn, streamID, client, serverConnID);
       }
 
-      flushEgress(sock, conn->get().d_conn, client, localAddr, buffer);
+      flushEgress(sock, conn->get().d_conn, client, localAddr, buffer, clientState.local.isUnspecified());
     }
     else {
       DEBUGLOG("Connection not established");
@@ -811,7 +811,7 @@
         for (auto conn = frontend->d_server_config->d_connections.begin(); conn != frontend->d_server_config->d_connections.end();) {
           quiche_conn_on_timeout(conn->second.d_conn.get());
 
-          flushEgress(sock, conn->second.d_conn, conn->second.d_peer, conn->second.d_localAddr, buffer);
+          flushEgress(sock, conn->second.d_conn, conn->second.d_peer, conn->second.d_localAddr, buffer, clientState->local.isUnspecified());
 
           if (quiche_conn_is_closed(conn->second.d_conn.get())) {
 #ifdef DEBUGLOG_ENABLED
diff -Nru dnsdist-1.9.9/doq-common.cc dnsdist-1.9.10/doq-common.cc
--- dnsdist-1.9.9/doq-common.cc	2025-04-29 11:46:04.000000000 +0200
+++ dnsdist-1.9.10/doq-common.cc	2025-05-20 11:13:25.000000000 +0200
@@ -126,10 +126,22 @@
   }
 }
 
-static void sendFromTo(Socket& sock, const ComboAddress& peer, const ComboAddress& local, PacketBuffer& buffer)
+static void sendFromTo(Socket& sock, const ComboAddress& peer, const ComboAddress& local, PacketBuffer& buffer, [[maybe_unused]] bool socketBoundToAny)
 {
-  const int flags = 0;
-  if (local.sin4.sin_family == 0) {
+  /* we only want to specify the source address to use if we were able to
+     either harvest it from the incoming packet, or if our socket is already
+     bound to a specific address */
+  bool setSourceAddress = local.sin4.sin_family != 0;
+#if defined(__FreeBSD__) || defined(__DragonFly__)
+  /* FreeBSD and DragonFlyBSD refuse the use of IP_SENDSRCADDR on a socket that is bound to a
+     specific address, returning EINVAL in that case. */
+  if (!socketBoundToAny) {
+    setSourceAddress = false;
+  }
+#endif /* __FreeBSD__ || __DragonFly__ */
+
+  if (!setSourceAddress) {
+    const int flags = 0;
     // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
     auto ret = sendto(sock.getHandle(), buffer.data(), buffer.size(), flags, reinterpret_cast<const struct sockaddr*>(&peer), peer.getSocklen());
     if (ret < 0) {
@@ -147,7 +159,7 @@
   }
 }
 
-void handleStatelessRetry(Socket& sock, const PacketBuffer& clientConnID, const PacketBuffer& serverConnID, const ComboAddress& peer, const ComboAddress& localAddr, uint32_t version, PacketBuffer& buffer)
+void handleStatelessRetry(Socket& sock, const PacketBuffer& clientConnID, const PacketBuffer& serverConnID, const ComboAddress& peer, const ComboAddress& localAddr, uint32_t version, PacketBuffer& buffer, bool socketBoundToAny)
 {
   auto newServerConnID = getCID();
   if (!newServerConnID) {
@@ -170,10 +182,10 @@
   }
 
   buffer.resize(static_cast<size_t>(written));
-  sendFromTo(sock, peer, localAddr, buffer);
+  sendFromTo(sock, peer, localAddr, buffer, socketBoundToAny);
 }
 
-void handleVersionNegociation(Socket& sock, const PacketBuffer& clientConnID, const PacketBuffer& serverConnID, const ComboAddress& peer, const ComboAddress& localAddr, PacketBuffer& buffer)
+void handleVersionNegotiation(Socket& sock, const PacketBuffer& clientConnID, const PacketBuffer& serverConnID, const ComboAddress& peer, const ComboAddress& localAddr, PacketBuffer& buffer, bool socketBoundToAny)
 {
   buffer.resize(MAX_DATAGRAM_SIZE);
 
@@ -187,10 +199,10 @@
   }
 
   buffer.resize(static_cast<size_t>(written));
-  sendFromTo(sock, peer, localAddr, buffer);
+  sendFromTo(sock, peer, localAddr, buffer, socketBoundToAny);
 }
 
-void flushEgress(Socket& sock, QuicheConnection& conn, const ComboAddress& peer, const ComboAddress& localAddr, PacketBuffer& buffer)
+void flushEgress(Socket& sock, QuicheConnection& conn, const ComboAddress& peer, const ComboAddress& localAddr, PacketBuffer& buffer, bool socketBoundToAny)
 {
   buffer.resize(MAX_DATAGRAM_SIZE);
   quiche_send_info send_info;
@@ -206,7 +218,7 @@
     }
     // FIXME pacing (as send_info.at should tell us when to send the packet) ?
     buffer.resize(static_cast<size_t>(written));
-    sendFromTo(sock, peer, localAddr, buffer);
+    sendFromTo(sock, peer, localAddr, buffer, socketBoundToAny);
   }
 }
 
@@ -312,9 +324,7 @@
        This is indicated by setting the family to 0 which is acted upon
        in sendUDPResponse() and DelayedPacket::().
     */
-    const ComboAddress bogusV4("0.0.0.0:0");
-    const ComboAddress bogusV6("[::]:0");
-    if ((localAddr.sin4.sin_family == AF_INET && localAddr == bogusV4) || (localAddr.sin4.sin_family == AF_INET6 && localAddr == bogusV6)) {
+    if (localAddr.isUnspecified()) {
       localAddr.sin4.sin_family = 0;
     }
   }
diff -Nru dnsdist-1.9.9/doq-common.hh dnsdist-1.9.10/doq-common.hh
--- dnsdist-1.9.9/doq-common.hh	2025-04-29 11:46:04.000000000 +0200
+++ dnsdist-1.9.10/doq-common.hh	2025-05-20 11:13:25.000000000 +0200
@@ -92,9 +92,9 @@
 std::optional<PacketBuffer> getCID();
 PacketBuffer mintToken(const PacketBuffer& dcid, const ComboAddress& peer);
 std::optional<PacketBuffer> validateToken(const PacketBuffer& token, const ComboAddress& peer);
-void handleStatelessRetry(Socket& sock, const PacketBuffer& clientConnID, const PacketBuffer& serverConnID, const ComboAddress& peer, const ComboAddress& localAddr, uint32_t version, PacketBuffer& buffer);
-void handleVersionNegociation(Socket& sock, const PacketBuffer& clientConnID, const PacketBuffer& serverConnID, const ComboAddress& peer, const ComboAddress& localAddr, PacketBuffer& buffer);
-void flushEgress(Socket& sock, QuicheConnection& conn, const ComboAddress& peer, const ComboAddress& localAddr, PacketBuffer& buffer);
+void handleStatelessRetry(Socket& sock, const PacketBuffer& clientConnID, const PacketBuffer& serverConnID, const ComboAddress& peer, const ComboAddress& localAddr, uint32_t version, PacketBuffer& buffer, bool socketBoundToAny);
+void handleVersionNegotiation(Socket& sock, const PacketBuffer& clientConnID, const PacketBuffer& serverConnID, const ComboAddress& peer, const ComboAddress& localAddr, PacketBuffer& buffer, bool socketBoundToAny);
+void flushEgress(Socket& sock, QuicheConnection& conn, const ComboAddress& peer, const ComboAddress& localAddr, PacketBuffer& buffer, bool socketBoundToAny);
 void configureQuiche(QuicheConfig& config, const QuicheParams& params, bool isHTTP);
 bool recvAsync(Socket& socket, PacketBuffer& buffer, ComboAddress& clientAddr, ComboAddress& localAddr);
 
diff -Nru dnsdist-1.9.9/iputils.hh dnsdist-1.9.10/iputils.hh
--- dnsdist-1.9.9/iputils.hh	2025-04-29 11:46:04.000000000 +0200
+++ dnsdist-1.9.10/iputils.hh	2025-05-20 11:13:25.000000000 +0200
@@ -266,6 +266,13 @@
     return true;
   }
 
+  [[nodiscard]] bool isUnspecified() const
+  {
+    const ComboAddress unspecifiedV4("0.0.0.0:0");
+    const ComboAddress unspecifiedV6("[::]:0");
+    return *this == unspecifiedV4 || *this == unspecifiedV6;
+  }
+
   ComboAddress mapToIPv4() const
   {
     if(!isMappedIPv4())
diff -Nru dnsdist-1.9.9/noinitvector.hh dnsdist-1.9.10/noinitvector.hh
--- dnsdist-1.9.9/noinitvector.hh	2025-04-29 11:46:04.000000000 +0200
+++ dnsdist-1.9.10/noinitvector.hh	2025-05-20 11:13:25.000000000 +0200
@@ -1,5 +1,6 @@
 #pragma once
 
+#include <cstdint>
 #include <memory>
 #include <new>
 #include <utility>
diff -Nru dnsdist-1.9.9/test-dnsdist-lua-ffi.cc dnsdist-1.9.10/test-dnsdist-lua-ffi.cc
--- dnsdist-1.9.9/test-dnsdist-lua-ffi.cc	2025-04-29 11:46:04.000000000 +0200
+++ dnsdist-1.9.10/test-dnsdist-lua-ffi.cc	2025-05-20 11:13:25.000000000 +0200
@@ -373,6 +373,30 @@
   BOOST_CHECK_EQUAL(ids.d_protoBufData->d_deviceID, deviceID);
   BOOST_CHECK_EQUAL(ids.d_protoBufData->d_deviceName, deviceName);
   BOOST_CHECK_EQUAL(ids.d_protoBufData->d_requestorID, requestorID);
+
+  /* no frontend yet */
+  BOOST_CHECK(dnsdist_ffi_dnsquestion_get_incoming_interface(nullptr) == nullptr);
+  BOOST_CHECK(dnsdist_ffi_dnsquestion_get_incoming_interface(&lightDQ) == nullptr);
+  {
+    /* frontend without and interface set */
+    const std::string interface{};
+    ClientState frontend(ids.origDest, false, false, 0, interface, {}, false);
+    ids.cs = &frontend;
+    const auto* itfPtr = dnsdist_ffi_dnsquestion_get_incoming_interface(&lightDQ);
+    BOOST_REQUIRE(itfPtr != nullptr);
+    BOOST_CHECK_EQUAL(std::string(itfPtr), interface);
+    ids.cs = nullptr;
+  }
+  {
+    /* frontend with interface set */
+    const std::string interface{"interface-name-0"};
+    ClientState frontend(ids.origDest, false, false, 0, interface, {}, false);
+    ids.cs = &frontend;
+    const auto* itfPtr = dnsdist_ffi_dnsquestion_get_incoming_interface(&lightDQ);
+    BOOST_REQUIRE(itfPtr != nullptr);
+    BOOST_CHECK_EQUAL(std::string(itfPtr), interface);
+    ids.cs = nullptr;
+  }
 }
 
 BOOST_AUTO_TEST_CASE(test_Response)
diff -Nru dnsdist-1.9.9/test-dnsdisttcp_cc.cc dnsdist-1.9.10/test-dnsdisttcp_cc.cc
--- dnsdist-1.9.9/test-dnsdisttcp_cc.cc	2025-04-29 11:46:04.000000000 +0200
+++ dnsdist-1.9.10/test-dnsdisttcp_cc.cc	2025-05-20 11:13:25.000000000 +0200
@@ -4182,4 +4182,82 @@
   }
 }
 
+BOOST_FIXTURE_TEST_CASE(test_Pipelined_Queries_Immediate_Responses, TestFixture)
+{
+  auto local = getBackendAddress("1", 80);
+  ClientState localCS(local, true, false, 0, "", {}, true);
+  auto tlsCtx = std::make_shared<MockupTLSCtx>();
+  localCS.tlsFrontend = std::make_shared<TLSFrontend>(tlsCtx);
+
+  TCPClientThreadData threadData;
+  threadData.mplexer = std::make_unique<MockupFDMultiplexer>();
+
+  timeval now{};
+  gettimeofday(&now, nullptr);
+
+  PacketBuffer query;
+  GenericDNSPacketWriter<PacketBuffer> pwQ(query, DNSName("powerdns.com."), QType::A, QClass::IN, 0);
+  pwQ.getHeader()->rd = 1;
+  pwQ.getHeader()->id = 0;
+
+  auto querySize = static_cast<uint16_t>(query.size());
+  const std::array<uint8_t, 2> sizeBytes{ static_cast<uint8_t>(querySize / 256), static_cast<uint8_t>(querySize % 256) };
+  query.insert(query.begin(), sizeBytes.begin(), sizeBytes.end());
+
+  auto backend = std::make_shared<DownstreamState>(getBackendAddress("42", 53));
+  backend->d_tlsCtx = tlsCtx;
+
+  {
+    /* 1000 queries from client passed to backend (one at at time), backend answers right away */
+    TEST_INIT("=> Query to backend, backend answers right away");
+    const size_t nbQueries = 10000;
+    s_readBuffer = query;
+    s_backendReadBuffer = query;
+
+    s_steps = {
+      { ExpectedStep::ExpectedRequest::handshakeClient, IOState::Done },
+      { ExpectedStep::ExpectedRequest::readFromClient, IOState::Done, 2 },
+      { ExpectedStep::ExpectedRequest::readFromClient, IOState::Done, query.size() - 2 },
+      /* opening a connection to the backend */
+      { ExpectedStep::ExpectedRequest::connectToBackend, IOState::Done },
+      { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, query.size() },
+      { ExpectedStep::ExpectedRequest::readFromBackend, IOState::Done, 2 },
+      { ExpectedStep::ExpectedRequest::readFromBackend, IOState::Done, query.size() - 2 },
+      { ExpectedStep::ExpectedRequest::writeToClient, IOState::Done, query.size() },
+    };
+    for (size_t idx = 1; idx < nbQueries; idx++) {
+      appendPayloadEditingID(s_readBuffer, query, idx);
+      appendPayloadEditingID(s_backendReadBuffer, query, idx);
+
+      s_steps.emplace_back(ExpectedStep::ExpectedRequest::readFromClient, IOState::Done, 2);
+      s_steps.emplace_back(ExpectedStep::ExpectedRequest::readFromClient, IOState::Done, query.size() - 2);
+      s_steps.emplace_back(ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, query.size());
+      s_steps.emplace_back(ExpectedStep::ExpectedRequest::readFromBackend, IOState::Done, 2);
+      s_steps.emplace_back(ExpectedStep::ExpectedRequest::readFromBackend, IOState::Done, query.size() - 2);
+      s_steps.emplace_back(ExpectedStep::ExpectedRequest::writeToClient, IOState::Done, query.size());
+    }
+    s_steps.emplace_back(ExpectedStep::ExpectedRequest::readFromClient, IOState::Done, 0);
+    /* closing client connection */
+    s_steps.emplace_back(ExpectedStep::ExpectedRequest::closeClient, IOState::Done);
+    /* closing a connection to the backend */
+    s_steps.emplace_back(ExpectedStep::ExpectedRequest::closeBackend, IOState::Done);
+
+    s_processQuery = [backend](DNSQuestion&, std::shared_ptr<DownstreamState>& selectedBackend) -> ProcessQueryResult {
+      selectedBackend = backend;
+      return ProcessQueryResult::PassToBackend;
+    };
+    s_processResponse = [](PacketBuffer&, DNSResponse&, bool) -> bool {
+      return true;
+    };
+
+    auto state = std::make_shared<IncomingTCPConnectionState>(ConnectionInfo(&localCS, getBackendAddress("84", 4242)), threadData, now);
+    state->handleIO();
+    BOOST_CHECK_EQUAL(s_writeBuffer.size(), query.size() * nbQueries);
+    BOOST_CHECK_EQUAL(s_backendWriteBuffer.size(), query.size() * nbQueries);
+    BOOST_CHECK_EQUAL(backend->outstanding.load(), 0U);
+    /* we need to clear them now, otherwise we end up with dangling pointers to the steps via the TLS context, etc */
+    IncomingTCPConnectionState::clearAllDownstreamConnections();
+  }
+}
+
 BOOST_AUTO_TEST_SUITE_END();

Reply to: