]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Implement a limit of concurrent connections to a backend
authorRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 20 Jun 2022 15:52:55 +0000 (17:52 +0200)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Fri, 24 Jun 2022 12:10:50 +0000 (14:10 +0200)
pdns/dnsdist-lua.cc
pdns/dnsdist-tcp.cc
pdns/dnsdistdist/dnsdist-downstream-connection.hh
pdns/dnsdistdist/dnsdist-tcp-downstream.cc
pdns/dnsdistdist/docs/reference/config.rst
pdns/dnsdistdist/test-dnsdist-connections-cache.cc
pdns/dnsdistdist/test-dnsdisttcp_cc.cc

index ec50b3b4b16731a5e6f96fc2d2012028feb28346..d77ddf51cb576e4169265f6c19431905d6c4a015 100644 (file)
@@ -454,6 +454,10 @@ static void setupLuaConfig(LuaContext& luaCtx, bool client, bool configCheck)
                            config.d_maxInFlightQueriesPerConn = std::stoi(boost::get<string>(vars["maxInFlight"]));
                          }
 
+                         if (vars.count("maxConcurrentTCPConnections")) {
+                           config.d_tcpConcurrentConnectionsLimit = std::stoi(boost::get<string>(vars.at("maxConcurrentTCPConnections")));
+                         }
+
                          if (vars.count("name")) {
                            config.name = boost::get<string>(vars["name"]);
                          }
index c46d766927042d9611f191b29dffec4e45066a40..311b5c7ef54e27b1471dffe24717c28484e79dc9 100644 (file)
@@ -238,7 +238,7 @@ static IOState sendQueuedResponses(std::shared_ptr<IncomingTCPConnectionState>&
   return IOState::Done;
 }
 
-static void handleResponseSent(std::shared_ptr<IncomingTCPConnectionState>& state, const TCPResponse& currentResponse)
+static void handleResponseSent(std::shared_ptr<IncomingTCPConnectionState>& state, TCPResponse& currentResponse)
 {
   if (currentResponse.d_idstate.qtype == QType::AXFR || currentResponse.d_idstate.qtype == QType::IXFR) {
     return;
@@ -261,6 +261,9 @@ static void handleResponseSent(std::shared_ptr<IncomingTCPConnectionState>& stat
     const auto& ids = currentResponse.d_idstate;
     ::handleResponseSent(ids, 0., state->d_ci.remote, ComboAddress(), static_cast<unsigned int>(currentResponse.d_buffer.size()), currentResponse.d_cleartextDH, ids.protocol);
   }
+
+  currentResponse.d_buffer.clear();
+  currentResponse.d_connection.reset();
 }
 
 static void prependSizeToTCPQuery(PacketBuffer& buffer, size_t proxyProtocolPayloadSize)
index 2d71919e646eef58d3751b918aa1f3cbc39f94db..b5c6a361a41364cf71ced42c84987938c830df67 100644 (file)
@@ -78,7 +78,12 @@ public:
       }
     }
 
+    if (ds->d_config.d_tcpConcurrentConnectionsLimit > 0 && ds->tcpCurrentConnections.load() >= ds->d_config.d_tcpConcurrentConnectionsLimit) {
+      throw std::runtime_error("Maximum number of TCP connections to " + ds->getNameWithAddr() + " reached, not creating a new one");
+    }
+
     auto newConnection = std::make_shared<T>(ds, mplexer, now, std::move(proxyProtocolPayload));
+    // might make sense to check whether max in flight > 0?
     if (!haveProxyProtocol) {
       auto& list = d_downstreamConnections[backendId].d_actives;
       list.template get<SequencedTag>().push_front(newConnection);
@@ -196,6 +201,8 @@ public:
     backendIt->second.d_actives.erase(it);
 
     if (backendIt->second.d_idles.size() >= s_maxIdleConnectionsPerDownstream) {
+      auto old = backendIt->second.d_idles.template get<SequencedTag>().back();
+      old->release();
       backendIt->second.d_idles.template get<SequencedTag>().pop_back();
     }
 
@@ -223,6 +230,7 @@ protected:
 
       if (entry->isIdle() && entry->getLastDataReceivedTime() < idleCutOff) {
         /* idle for too long */
+        (*connIt)->release();
         connIt = sidx.erase(connIt);
         continue;
       }
@@ -232,6 +240,7 @@ protected:
         continue;
       }
 
+      (*connIt)->release();
       connIt = sidx.erase(connIt);
     }
   }
index cfb6b98e6e558d1d8fdb92a445486b81359fe586..418bf6b5f0e296fcd6587b69ba6e7be953c7728f 100644 (file)
@@ -338,7 +338,7 @@ void TCPConnectionToBackend::handleIO(std::shared_ptr<TCPConnectionToBackend>& c
 
     if (connectionDied) {
 
-      DEBUGLOG("connection died, number of failures is "<<conn->d_downstreamFailures<<", retries is "<<conn->d_ds->d_retries);
+      DEBUGLOG("connection died, number of failures is "<<conn->d_downstreamFailures<<", retries is "<<conn->d_ds->d_config.d_retries);
 
       if (conn->d_downstreamFailures < conn->d_ds->d_config.d_retries) {
 
@@ -640,15 +640,15 @@ IOState TCPConnectionToBackend::handleResponse(std::shared_ptr<TCPConnectionToBa
       --conn->d_ds->outstanding;
       /* marking as idle for now, so we can accept new queries if our queues are empty */
       if (d_pendingQueries.empty() && d_pendingResponses.empty()) {
-        t_downstreamTCPConnectionsManager.moveToIdle(conn);
         d_state = State::idle;
+        t_downstreamTCPConnectionsManager.moveToIdle(conn);
       }
     }
 
     sender->handleXFRResponse(now, std::move(response));
     if (done) {
-      t_downstreamTCPConnectionsManager.moveToIdle(conn);
       d_state = State::idle;
+      t_downstreamTCPConnectionsManager.moveToIdle(conn);
       return IOState::Done;
     }
 
@@ -667,8 +667,8 @@ IOState TCPConnectionToBackend::handleResponse(std::shared_ptr<TCPConnectionToBa
   d_pendingResponses.erase(it);
   /* marking as idle for now, so we can accept new queries if our queues are empty */
   if (d_pendingQueries.empty() && d_pendingResponses.empty()) {
-    t_downstreamTCPConnectionsManager.moveToIdle(conn);
     d_state = State::idle;
+    t_downstreamTCPConnectionsManager.moveToIdle(conn);
   }
 
   auto shared = conn;
@@ -691,8 +691,8 @@ IOState TCPConnectionToBackend::handleResponse(std::shared_ptr<TCPConnectionToBa
   }
   else {
     DEBUGLOG("nothing to do, waiting for a new query");
-    t_downstreamTCPConnectionsManager.moveToIdle(conn);
     d_state = State::idle;
+    t_downstreamTCPConnectionsManager.moveToIdle(conn);
     return IOState::Done;
   }
 }
index 0b548086b95d115624d042eea2c5bb421657f994..1fbd04211472fcdc0ad987e80f30fb8be3fff2e7 100644 (file)
@@ -537,7 +537,7 @@ Servers
     Added ``addXForwardedHeaders``, ``caStore``, ``checkTCP``, ``ciphers``, ``ciphers13``, ``dohPath``, ``enableRenegotiation``, ``releaseBuffers``, ``subjectName``, ``tcpOnly``, ``tls`` and ``validateCertificates`` to server_table.
 
   .. versionchanged:: 1.8.0
-    Added ``autoUpgrade``, ``autoUpgradeDoHKey``, ``autoUpgradeInterval``, ``autoUpgradeKeep``, ``autoUpgradePool`` and ``subjectAddr`` to server_table.
+    Added ``autoUpgrade``, ``autoUpgradeDoHKey``, ``autoUpgradeInterval``, ``autoUpgradeKeep``, ``autoUpgradePool``, ``maxConcurrentTCPConnections`` and ``subjectAddr`` to server_table.
 
   Add a new backend server. Call this function with either a string::
 
@@ -548,61 +548,62 @@ Servers
   or a table::
 
     newServer({
-      address="IP:PORT",        -- IP and PORT of the backend server (mandatory)
-      id=STRING,                -- Use a pre-defined UUID instead of a random one
-      qps=NUM,                  -- Limit the number of queries per second to NUM, when using the `firstAvailable` policy
-      order=NUM,                -- The order of this server, used by the `leastOutstanding` and `firstAvailable` policies
-      weight=NUM,               -- The weight of this server, used by the `wrandom`, `whashed` and `chashed` policies, default: 1
-                                -- Supported values are a minimum of 1, and a maximum of 2147483647.
-      pool=STRING|{STRING},     -- The pools this server belongs to (unset or empty string means default pool) as a string or table of strings
-      retries=NUM,              -- The number of TCP connection attempts to the backend, for a given query
-      tcpConnectTimeout=NUM,    -- The timeout (in seconds) of a TCP connection attempt
-      tcpSendTimeout=NUM,       -- The timeout (in seconds) of a TCP write attempt
-      tcpRecvTimeout=NUM,       -- The timeout (in seconds) of a TCP read attempt
-      tcpFastOpen=BOOL,         -- Whether to enable TCP Fast Open
-      ipBindAddrNoPort=BOOL,    -- Whether to enable IP_BIND_ADDRESS_NO_PORT if available, default: true
-      name=STRING,              -- The name associated to this backend, for display purpose
-      checkClass=NUM,           -- Use NUM as QCLASS in the health-check query, default: DNSClass.IN
-      checkName=STRING,         -- Use STRING as QNAME in the health-check query, default: "a.root-servers.net."
-      checkType=STRING,         -- Use STRING as QTYPE in the health-check query, default: "A"
-      checkFunction=FUNCTION,   -- Use this function to dynamically set the QNAME, QTYPE and QCLASS to use in the health-check query (see :ref:`Healthcheck`)
-      checkTimeout=NUM,         -- The timeout (in milliseconds) of a health-check query, default: 1000 (1s)
-      setCD=BOOL,               -- Set the CD (Checking Disabled) flag in the health-check query, default: false
-      maxCheckFailures=NUM,     -- Allow NUM check failures before declaring the backend down, default: 1
-      checkInterval=NUM         -- The time in seconds between health checks
-      mustResolve=BOOL,         -- Set to true when the health check MUST return a RCODE different from NXDomain, ServFail and Refused. Default is false, meaning that every RCODE except ServFail is considered valid
-      useClientSubnet=BOOL,     -- Add the client's IP address in the EDNS Client Subnet option when forwarding the query to this backend
-      source=STRING,            -- The source address or interface to use for queries to this backend, by default this is left to the kernel's address selection
-                                -- The following formats are supported:
-                                --   "address", e.g. "192.0.2.2"
-                                --   "interface name", e.g. "eth0"
-                                --   "address@interface", e.g. "192.0.2.2@eth0"
-      addXPF=NUM,               -- Add the client's IP address and port to the query, along with the original destination address and port,
-                                -- using the experimental XPF record from `draft-bellis-dnsop-xpf <https://datatracker.ietf.org/doc/draft-bellis-dnsop-xpf/>`_ and the specified option code. Default is disabled (0). This is a deprecated feature that will be removed in the near future.
-      sockets=NUM,              -- Number of UDP sockets (and thus source ports) used toward the backend server, defaults to a single one. Note that for backends which are multithreaded, this setting will have an effect on the number of cores that will be used to process traffic from dnsdist. For example you may want to set 'sockets' to a number somewhat higher than the number of worker threads configured in the backend, particularly if the Linux kernel is being used to distribute traffic to multiple threads listening on the same socket (via `reuseport`).
-      disableZeroScope=BOOL,    -- Disable the EDNS Client Subnet 'zero scope' feature, which does a cache lookup for an answer valid for all subnets (ECS scope of 0) before adding ECS information to the query and doing the regular lookup. This requires the ``parseECS`` option of the corresponding cache to be set to true
-      rise=NUM,                 -- Require NUM consecutive successful checks before declaring the backend up, default: 1
-      useProxyProtocol=BOOL,    -- Add a proxy protocol header to the query, passing along the client's IP address and port along with the original destination address and port. Default is disabled.
-      reconnectOnUp=BOOL,       -- Close and reopen the sockets when a server transits from Down to Up. This helps when an interface is missing when dnsdist is started. Default is disabled.
-      maxInFlight=NUM,          -- Maximum number of in-flight queries. The default is 0, which disables out-of-order processing. It should only be enabled if the backend does support out-of-order processing. As of 1.6.0, out-of-order processing needs to be enabled on the frontend as well, via :func:`addLocal` and/or :func:`addTLSLocal`. Note that out-of-order is always enabled on DoH frontends.
-      tcpOnly=BOOL,             -- Always forward queries to that backend over TCP, never over UDP. Always enabled for TLS backends. Default is false.
-      checkTCP=BOOL,            -- Whether to do healthcheck queries over TCP, instead of UDP. Always enabled for DNS over TLS backend. Default is false.
-      tls=STRING,               -- Enable DNS over TLS communications for this backend, or DNS over HTTPS if ``dohPath`` is set, using the TLS provider ("openssl" or "gnutls") passed in parameter. Default is an empty string, which means this backend is used for plain UDP and TCP.
-      caStore=STRING,           -- Specifies the path to the CA certificate file, in PEM format, to use to check the certificate presented by the backend. Default is an empty string, which means to use the system CA store. Note that this directive is only used if ``validateCertificates`` is set.
-      ciphers=STRING,           -- The TLS ciphers to use. The exact format depends on the provider used. When the OpenSSL provider is used, ciphers for TLS 1.3 must be specified via ``ciphersTLS13``.
-      ciphersTLS13=STRING,      -- The ciphers to use for TLS 1.3, when the OpenSSL provider is used. When the GnuTLS provider is used, ``ciphers`` applies regardless of the TLS protocol and this setting is not used.
-      subjectName=STRING,       -- The subject name passed in the SNI value of the TLS handshake, and against which to validate the certificate presented by the backend. Default is empty. If set this value supersedes any ``subjectAddr`` one.
-      subjectAddr=STRING,       -- The subject IP address passed in the SNI value of the TLS handshake, and against which to validate the certificate presented by the backend. Default is empty.
-      validateCertificates=BOOL,-- Whether the certificate presented by the backend should be validated against the CA store (see ``caStore``). Default is true.
-      dohPath=STRING,           -- Enable DNS over HTTPS communication for this backend, using POST queries to the HTTP host supplied as ``subjectName`` and the HTTP path supplied in this parameter.
-      addXForwardedHeaders=BOOL,-- Whether to add X-Forwarded-For, X-Forwarded-Port and X-Forwarded-Proto headers to a DNS over HTTPS backend.
-      releaseBuffers=BOOL,      -- Whether OpenSSL should release its I/O buffers when a connection goes idle, saving roughly 35 kB of memory per connection. Default to true.
-      enableRenegotiation=BOOL, -- Whether secure TLS renegotiation should be enabled. Disabled by default since it increases the attack surface and is seldom used for DNS.
-      autoUpgrade=BOOL,         -- Whether to use the 'Discovery of Designated Resolvers' mechanism to automatically upgrade a Do53 backend to DoT or DoH, depending on the priorities present in the SVCB record returned by the backend. Default to false.
-      autoUpgradeInterval=NUM,  -- If ``autoUpgrade`` is set, how often to check if an upgrade is available, in seconds. Default is 3600 seconds.
-      autoUpgradeKeep=BOOL,     -- If ``autoUpgrade`` is set, whether to keep the existing Do53 backend around after an upgrade. Default is false which means the Do53 backend will be replaced by the upgraded one.
-      autoUpgradePool=STRING,   -- If ``autoUpgrade`` is set, in which pool to place the newly upgraded backend. Default is empty which means the backend is placed in the default pool.
-      autoUpgradeDoHKey=NUM     -- If ``autoUpgrade`` is set, the value to use for the SVC key corresponding to the DoH path. Default is 7.
+      address="IP:PORT",               -- IP and PORT of the backend server (mandatory)
+      id=STRING,                       -- Use a pre-defined UUID instead of a random one
+      qps=NUM,                         -- Limit the number of queries per second to NUM, when using the `firstAvailable` policy
+      order=NUM,                       -- The order of this server, used by the `leastOutstanding` and `firstAvailable` policies
+      weight=NUM,                      -- The weight of this server, used by the `wrandom`, `whashed` and `chashed` policies, default: 1
+                                       -- Supported values are a minimum of 1, and a maximum of 2147483647.
+      pool=STRING|{STRING},            -- The pools this server belongs to (unset or empty string means default pool) as a string or table of strings
+      retries=NUM,                     -- The number of TCP connection attempts to the backend, for a given query
+      tcpConnectTimeout=NUM,           -- The timeout (in seconds) of a TCP connection attempt
+      tcpSendTimeout=NUM,              -- The timeout (in seconds) of a TCP write attempt
+      tcpRecvTimeout=NUM,              -- The timeout (in seconds) of a TCP read attempt
+      tcpFastOpen=BOOL,                -- Whether to enable TCP Fast Open
+      ipBindAddrNoPort=BOOL,           -- Whether to enable IP_BIND_ADDRESS_NO_PORT if available, default: true
+      name=STRING,                     -- The name associated to this backend, for display purpose
+      checkClass=NUM,                  -- Use NUM as QCLASS in the health-check query, default: DNSClass.IN
+      checkName=STRING,                -- Use STRING as QNAME in the health-check query, default: "a.root-servers.net."
+      checkType=STRING,                -- Use STRING as QTYPE in the health-check query, default: "A"
+      checkFunction=FUNCTION,          -- Use this function to dynamically set the QNAME, QTYPE and QCLASS to use in the health-check query (see :ref:`Healthcheck`)
+      checkTimeout=NUM,                -- The timeout (in milliseconds) of a health-check query, default: 1000 (1s)
+      setCD=BOOL,                      -- Set the CD (Checking Disabled) flag in the health-check query, default: false
+      maxCheckFailures=NUM,            -- Allow NUM check failures before declaring the backend down, default: 1
+      checkInterval=NUM                -- The time in seconds between health checks
+      mustResolve=BOOL,                -- Set to true when the health check MUST return a RCODE different from NXDomain, ServFail and Refused. Default is false, meaning that every RCODE except ServFail is considered valid
+      useClientSubnet=BOOL,            -- Add the client's IP address in the EDNS Client Subnet option when forwarding the query to this backend
+      source=STRING,                   -- The source address or interface to use for queries to this backend, by default this is left to the kernel's address selection
+                                       -- The following formats are supported:
+                                       --   "address", e.g. "192.0.2.2"
+                                       --   "interface name", e.g. "eth0"
+                                       --   "address@interface", e.g. "192.0.2.2@eth0"
+      addXPF=NUM,                      -- Add the client's IP address and port to the query, along with the original destination address and port,
+                                       -- using the experimental XPF record from `draft-bellis-dnsop-xpf <https://datatracker.ietf.org/doc/draft-bellis-dnsop-xpf/>`_ and the specified option code. Default is disabled (0). This is a deprecated feature that will be removed in the near future.
+      sockets=NUM,                     -- Number of UDP sockets (and thus source ports) used toward the backend server, defaults to a single one. Note that for backends which are multithreaded, this setting will have an effect on the number of cores that will be used to process traffic from dnsdist. For example you may want to set 'sockets' to a number somewhat higher than the number of worker threads configured in the backend, particularly if the Linux kernel is being used to distribute traffic to multiple threads listening on the same socket (via `reuseport`).
+      disableZeroScope=BOOL,           -- Disable the EDNS Client Subnet 'zero scope' feature, which does a cache lookup for an answer valid for all subnets (ECS scope of 0) before adding ECS information to the query and doing the regular lookup. This requires the ``parseECS`` option of the corresponding cache to be set to true
+      rise=NUM,                        -- Require NUM consecutive successful checks before declaring the backend up, default: 1
+      useProxyProtocol=BOOL,           -- Add a proxy protocol header to the query, passing along the client's IP address and port along with the original destination address and port. Default is disabled.
+      reconnectOnUp=BOOL,              -- Close and reopen the sockets when a server transits from Down to Up. This helps when an interface is missing when dnsdist is started. Default is disabled.
+      maxInFlight=NUM,                 -- Maximum number of in-flight queries. The default is 0, which disables out-of-order processing. It should only be enabled if the backend does support out-of-order processing. As of 1.6.0, out-of-order processing needs to be enabled on the frontend as well, via :func:`addLocal` and/or :func:`addTLSLocal`. Note that out-of-order is always enabled on DoH frontends.
+      tcpOnly=BOOL,                    -- Always forward queries to that backend over TCP, never over UDP. Always enabled for TLS backends. Default is false.
+      checkTCP=BOOL,                   -- Whether to do healthcheck queries over TCP, instead of UDP. Always enabled for DNS over TLS backend. Default is false.
+      tls=STRING,                      -- Enable DNS over TLS communications for this backend, or DNS over HTTPS if ``dohPath`` is set, using the TLS provider ("openssl" or "gnutls") passed in parameter. Default is an empty string, which means this backend is used for plain UDP and TCP.
+      caStore=STRING,                  -- Specifies the path to the CA certificate file, in PEM format, to use to check the certificate presented by the backend. Default is an empty string, which means to use the system CA store. Note that this directive is only used if ``validateCertificates`` is set.
+      ciphers=STRING,                  -- The TLS ciphers to use. The exact format depends on the provider used. When the OpenSSL provider is used, ciphers for TLS 1.3 must be specified via ``ciphersTLS13``.
+      ciphersTLS13=STRING,             -- The ciphers to use for TLS 1.3, when the OpenSSL provider is used. When the GnuTLS provider is used, ``ciphers`` applies regardless of the TLS protocol and this setting is not used.
+      subjectName=STRING,              -- The subject name passed in the SNI value of the TLS handshake, and against which to validate the certificate presented by the backend. Default is empty. If set this value supersedes any ``subjectAddr`` one.
+      subjectAddr=STRING,              -- The subject IP address passed in the SNI value of the TLS handshake, and against which to validate the certificate presented by the backend. Default is empty.
+      validateCertificates=BOOL,       -- Whether the certificate presented by the backend should be validated against the CA store (see ``caStore``). Default is true.
+      dohPath=STRING,                  -- Enable DNS over HTTPS communication for this backend, using POST queries to the HTTP host supplied as ``subjectName`` and the HTTP path supplied in this parameter.
+      addXForwardedHeaders=BOOL,       -- Whether to add X-Forwarded-For, X-Forwarded-Port and X-Forwarded-Proto headers to a DNS over HTTPS backend.
+      releaseBuffers=BOOL,             -- Whether OpenSSL should release its I/O buffers when a connection goes idle, saving roughly 35 kB of memory per connection. Default to true.
+      enableRenegotiation=BOOL,        -- Whether secure TLS renegotiation should be enabled. Disabled by default since it increases the attack surface and is seldom used for DNS.
+      autoUpgrade=BOOL,                -- Whether to use the 'Discovery of Designated Resolvers' mechanism to automatically upgrade a Do53 backend to DoT or DoH, depending on the priorities present in the SVCB record returned by the backend. Default to false.
+      autoUpgradeInterval=NUM,         -- If ``autoUpgrade`` is set, how often to check if an upgrade is available, in seconds. Default is 3600 seconds.
+      autoUpgradeKeep=BOOL,            -- If ``autoUpgrade`` is set, whether to keep the existing Do53 backend around after an upgrade. Default is false which means the Do53 backend will be replaced by the upgraded one.
+      autoUpgradePool=STRING,          -- If ``autoUpgrade`` is set, in which pool to place the newly upgraded backend. Default is empty which means the backend is placed in the default pool.
+      autoUpgradeDoHKey=NUM,           -- If ``autoUpgrade`` is set, the value to use for the SVC key corresponding to the DoH path. Default is 7.
+      maxConcurrentTCPConnections=NUM  -- Maximum number of TCP connections to that backend. When that limit is reached, queries routed to that backend that cannot be forwarded over an existing connection will be dropped. Default is 0 which means no limit.
     })
 
   :param str server_string: A simple IP:PORT string.
index 78c667c714ee71f059ba2c37fb7d01444936ea44..024d9db9405909e53a41f6b1a3c6fcb8c80fbe2e 100644 (file)
@@ -68,6 +68,10 @@ public:
   {
   }
 
+  void release()
+  {
+  }
+
   std::shared_ptr<DownstreamState> getDS() const
   {
     return d_ds;
index ca8ed42d94457844189ed8ea00f20549aed183d2..f3c8678a39ffceb27ced88088d0858160afb326c 100644 (file)
@@ -1676,10 +1676,10 @@ BOOST_AUTO_TEST_CASE(test_IncomingConnection_BackendNoOOOR)
       /* send the response */
       s_steps.push_back({ ExpectedStep::ExpectedRequest::writeToClient, IOState::Done, query.size() + 2 });
     };
+    /* close the connection with the backend */
+    s_steps.push_back({ ExpectedStep::ExpectedRequest::closeBackend, IOState::Done });
     /* close the connection with the client */
     s_steps.push_back({ ExpectedStep::ExpectedRequest::closeClient, IOState::Done });
-    /* eventually with the backend as well */
-    s_steps.push_back({ ExpectedStep::ExpectedRequest::closeBackend, IOState::Done });
 
     s_processQuery = [backend](DNSQuestion& dq, ClientState& cs, LocalHolders& holders, std::shared_ptr<DownstreamState>& selectedBackend) -> ProcessQueryResult {
       selectedBackend = backend;
@@ -2175,11 +2175,11 @@ BOOST_AUTO_TEST_CASE(test_IncomingConnectionOOOR_BackendOOOR)
         timeout = true;
       } },
 
-      /* closing client connection */
-      { ExpectedStep::ExpectedRequest::closeClient, IOState::Done },
-
       /* closing a connection to the backend */
       { ExpectedStep::ExpectedRequest::closeBackend, IOState::Done },
+
+      /* closing client connection */
+      { ExpectedStep::ExpectedRequest::closeClient, IOState::Done },
     };
 
     s_processQuery = [backend](DNSQuestion& dq, ClientState& cs, LocalHolders& holders, std::shared_ptr<DownstreamState>& selectedBackend) -> ProcessQueryResult {
@@ -2452,10 +2452,10 @@ BOOST_AUTO_TEST_CASE(test_IncomingConnectionOOOR_BackendOOOR)
       { ExpectedStep::ExpectedRequest::writeToClient, IOState::Done, responses.at(4).size(), [&timeout](int desc) {
         timeout = true;
       } },
-      /* client times out again, this time we close the connection */
-      { ExpectedStep::ExpectedRequest::closeClient, IOState::Done, 0 },
       /* closing a connection to the backend */
       { ExpectedStep::ExpectedRequest::closeBackend, IOState::Done },
+      /* client times out again, this time we close the connection */
+      { ExpectedStep::ExpectedRequest::closeClient, IOState::Done, 0 },
     };
 
     s_processQuery = [backend](DNSQuestion& dq, ClientState& cs, LocalHolders& holders, std::shared_ptr<DownstreamState>& selectedBackend) -> ProcessQueryResult {
@@ -3375,10 +3375,10 @@ BOOST_AUTO_TEST_CASE(test_IncomingConnectionOOOR_BackendOOOR)
       } },
       /* client closes the connection */
       { ExpectedStep::ExpectedRequest::readFromClient, IOState::Done, 0 },
-      /* closing the client connection */
-      { ExpectedStep::ExpectedRequest::closeClient, IOState::Done, 0 },
       /* closing the backend connection */
       { ExpectedStep::ExpectedRequest::closeBackend, IOState::Done, 0 },
+      /* closing the client connection */
+      { ExpectedStep::ExpectedRequest::closeClient, IOState::Done, 0 },
     };
 
     s_processQuery = [proxyEnabledBackend](DNSQuestion& dq, ClientState& cs, LocalHolders& holders, std::shared_ptr<DownstreamState>& selectedBackend) -> ProcessQueryResult {
@@ -3801,10 +3801,10 @@ BOOST_AUTO_TEST_CASE(test_IncomingConnectionOOOR_BackendOOOR)
       { ExpectedStep::ExpectedRequest::writeToClient, IOState::Done, responses.at(0).size(), [&timeout](int desc) {
         timeout = true;
       } },
-      /* closing client connection */
-      { ExpectedStep::ExpectedRequest::closeClient, IOState::Done },
       /* closing a connection to the backend */
       { ExpectedStep::ExpectedRequest::closeBackend, IOState::Done },
+      /* closing client connection */
+      { ExpectedStep::ExpectedRequest::closeClient, IOState::Done },
     };
 
     s_processQuery = [backend](DNSQuestion& dq, ClientState& cs, LocalHolders& holders, std::shared_ptr<DownstreamState>& selectedBackend) -> ProcessQueryResult {