/src/pdns/pdns/dnsdistdist/dnsdist-backend.cc
Line | Count | Source |
1 | | /* |
2 | | * This file is part of PowerDNS or dnsdist. |
3 | | * Copyright -- PowerDNS.COM B.V. and its contributors |
4 | | * |
5 | | * This program is free software; you can redistribute it and/or modify |
6 | | * it under the terms of version 2 of the GNU General Public License as |
7 | | * published by the Free Software Foundation. |
8 | | * |
9 | | * In addition, for the avoidance of any doubt, permission is granted to |
10 | | * link this program with OpenSSL and to (re)distribute the binaries |
11 | | * produced as the result of such linking. |
12 | | * |
13 | | * This program is distributed in the hope that it will be useful, |
14 | | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
15 | | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
16 | | * GNU General Public License for more details. |
17 | | * |
18 | | * You should have received a copy of the GNU General Public License |
19 | | * along with this program; if not, write to the Free Software |
20 | | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
21 | | */ |
22 | | |
23 | | // for OpenBSD, sys/socket.h needs to come before net/if.h |
24 | | #include <sys/socket.h> |
25 | | #include <net/if.h> |
26 | | |
27 | | #include <boost/format.hpp> |
28 | | |
29 | | #include "config.h" |
30 | | #include "dnsdist.hh" |
31 | | #include "dnsdist-backend.hh" |
32 | | #include "dnsdist-backoff.hh" |
33 | | #include "dnsdist-metrics.hh" |
34 | | #include "dnsdist-nghttp2.hh" |
35 | | #include "dnsdist-random.hh" |
36 | | #include "dnsdist-rings.hh" |
37 | | #include "dnsdist-snmp.hh" |
38 | | #include "dnsdist-tcp.hh" |
39 | | #include "dnsdist-udp.hh" |
40 | | #include "dnsdist-xsk.hh" |
41 | | #include "dolog.hh" |
42 | | #include "xsk.hh" |
43 | | |
44 | | bool DownstreamState::passCrossProtocolQuery(std::unique_ptr<CrossProtocolQuery>&& cpq) |
45 | 0 | { |
46 | 0 | auto closer = cpq->query.d_idstate.getCloser(__func__); // NOLINT(cppcoreguidelines-pro-bounds-array-to-pointer-decay) |
47 | | #if defined(HAVE_DNS_OVER_HTTPS) && defined(HAVE_NGHTTP2) |
48 | | if (!d_config.d_dohPath.empty()) { |
49 | | return g_dohClientThreads && g_dohClientThreads->passCrossProtocolQueryToThread(std::move(cpq)); |
50 | | } |
51 | | #endif |
52 | 0 | return g_tcpclientthreads && g_tcpclientthreads->passCrossProtocolQueryToThread(std::move(cpq)); |
53 | 0 | } |
54 | | |
55 | | #ifdef HAVE_XSK |
56 | | void DownstreamState::addXSKDestination(int fd) |
57 | | { |
58 | | auto socklen = d_config.remote.getSocklen(); |
59 | | ComboAddress local; |
60 | | // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): sorry, it's the API |
61 | | if (getsockname(fd, reinterpret_cast<sockaddr*>(&local), &socklen) != 0) { |
62 | | return; |
63 | | } |
64 | | |
65 | | { |
66 | | auto addresses = d_socketSourceAddresses.write_lock(); |
67 | | addresses->push_back(local); |
68 | | } |
69 | | dnsdist::xsk::addDestinationAddress(local); |
70 | | for (size_t idx = 0; idx < d_xskSockets.size(); idx++) { |
71 | | d_xskSockets.at(idx)->addWorkerRoute(d_xskInfos.at(idx), local); |
72 | | } |
73 | | } |
74 | | |
75 | | void DownstreamState::removeXSKDestination(int fd) |
76 | | { |
77 | | auto socklen = d_config.remote.getSocklen(); |
78 | | ComboAddress local; |
79 | | // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): sorry, it's the API |
80 | | if (getsockname(fd, reinterpret_cast<sockaddr*>(&local), &socklen) != 0) { |
81 | | return; |
82 | | } |
83 | | |
84 | | dnsdist::xsk::removeDestinationAddress(local); |
85 | | for (auto& xskSocket : d_xskSockets) { |
86 | | xskSocket->removeWorkerRoute(local); |
87 | | } |
88 | | } |
89 | | #endif /* HAVE_XSK */ |
90 | | |
91 | | bool DownstreamState::reconnect(bool initialAttempt) |
92 | 0 | { |
93 | 0 | std::unique_lock<std::mutex> lock(connectLock, std::try_to_lock); |
94 | 0 | if (!lock.owns_lock() || isStopped()) { |
95 | | /* we are already reconnecting or stopped anyway */ |
96 | 0 | return false; |
97 | 0 | } |
98 | | |
99 | 0 | if (IsAnyAddress(d_config.remote)) { |
100 | 0 | return true; |
101 | 0 | } |
102 | | |
103 | 0 | connected = false; |
104 | | #ifdef HAVE_XSK |
105 | | if (!d_xskInfos.empty()) { |
106 | | auto addresses = d_socketSourceAddresses.write_lock(); |
107 | | addresses->clear(); |
108 | | } |
109 | | #endif /* HAVE_XSK */ |
110 | |
|
111 | 0 | for (auto& fd : sockets) { |
112 | 0 | if (fd != -1) { |
113 | 0 | if (sockets.size() > 1) { |
114 | 0 | (*mplexer.lock())->removeReadFD(fd); |
115 | 0 | } |
116 | | #ifdef HAVE_XSK |
117 | | if (!d_xskInfos.empty()) { |
118 | | removeXSKDestination(fd); |
119 | | } |
120 | | #endif /* HAVE_XSK */ |
121 | | /* shutdown() is needed to wake up recv() in the responderThread */ |
122 | 0 | shutdown(fd, SHUT_RDWR); |
123 | 0 | close(fd); |
124 | 0 | fd = -1; |
125 | 0 | } |
126 | 0 | fd = SSocket(d_config.remote.sin4.sin_family, SOCK_DGRAM, 0); |
127 | |
|
128 | 0 | #ifdef SO_BINDTODEVICE |
129 | 0 | if (!d_config.sourceItfName.empty()) { |
130 | 0 | int res = setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, d_config.sourceItfName.c_str(), d_config.sourceItfName.length()); |
131 | 0 | if (res != 0) { |
132 | 0 | int saved = errno; |
133 | 0 | SLOG(infolog("Error setting up the interface on backend socket '%s': %s", d_config.remote.toStringWithPort(), stringerror(saved)), |
134 | 0 | getLogger()->error(Logr::Info, saved, "Error setting up the interface on backend socket", "source.interface", Logging::Loggable(d_config.sourceItfName))); |
135 | 0 | } |
136 | 0 | } |
137 | 0 | #endif |
138 | |
|
139 | 0 | dnsdist::udp::setUDPSocketBufferSizes(fd, *getLogger(), dnsdist::udp::Context::Backend, d_config.remote); |
140 | |
|
141 | 0 | if (!IsAnyAddress(d_config.sourceAddr)) { |
142 | 0 | #ifdef IP_BIND_ADDRESS_NO_PORT |
143 | 0 | if (d_config.ipBindAddrNoPort) { |
144 | 0 | SSetsockopt(fd, SOL_IP, IP_BIND_ADDRESS_NO_PORT, 1); |
145 | 0 | } |
146 | 0 | #endif |
147 | 0 | SBind(fd, d_config.sourceAddr); |
148 | 0 | } |
149 | |
|
150 | 0 | try { |
151 | 0 | setDscp(fd, d_config.remote.sin4.sin_family, d_config.dscp); |
152 | 0 | SConnect(fd, d_config.tcpFastOpen, d_config.remote); |
153 | 0 | if (sockets.size() > 1) { |
154 | 0 | (*mplexer.lock())->addReadFD(fd, [](int, boost::any) {}); |
155 | 0 | } |
156 | | #ifdef HAVE_XSK |
157 | | if (!d_xskInfos.empty()) { |
158 | | addXSKDestination(fd); |
159 | | } |
160 | | #endif /* HAVE_XSK */ |
161 | 0 | connected = true; |
162 | 0 | } |
163 | 0 | catch (const std::runtime_error& error) { |
164 | 0 | if (initialAttempt || dnsdist::configuration::getCurrentRuntimeConfiguration().d_verbose) { |
165 | 0 | if (!IsAnyAddress(d_config.sourceAddr) || !d_config.sourceItfName.empty()) { |
166 | 0 | SLOG(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()), |
167 | 0 | getLogger()->error(Logr::Info, error.what(), "Error connecting to server", "source.address", Logging::Loggable(d_config.sourceAddr), "source.interface", Logging::Loggable(d_config.sourceItfName))); |
168 | 0 | } |
169 | 0 | else { |
170 | 0 | SLOG(infolog("Error connecting to new server with address %s: %s", d_config.remote.toStringWithPort(), error.what()), |
171 | 0 | getLogger()->error(Logr::Info, error.what(), "Error connecting to server")); |
172 | 0 | } |
173 | 0 | } |
174 | 0 | connected = false; |
175 | 0 | break; |
176 | 0 | } |
177 | 0 | } |
178 | | |
179 | | /* if at least one (re-)connection failed, close all sockets */ |
180 | 0 | if (!connected) { |
181 | | #ifdef HAVE_XSK |
182 | | if (!d_xskInfos.empty()) { |
183 | | auto addresses = d_socketSourceAddresses.write_lock(); |
184 | | addresses->clear(); |
185 | | } |
186 | | #endif /* HAVE_XSK */ |
187 | 0 | for (auto& fd : sockets) { |
188 | 0 | if (fd != -1) { |
189 | | #ifdef HAVE_XSK |
190 | | if (!d_xskInfos.empty()) { |
191 | | removeXSKDestination(fd); |
192 | | } |
193 | | #endif /* HAVE_XSK */ |
194 | 0 | if (sockets.size() > 1) { |
195 | 0 | try { |
196 | 0 | (*mplexer.lock())->removeReadFD(fd); |
197 | 0 | } |
198 | 0 | catch (const FDMultiplexerException& e) { |
199 | | /* some sockets might not have been added to the multiplexer |
200 | | yet, that's fine */ |
201 | 0 | } |
202 | 0 | } |
203 | | /* shutdown() is needed to wake up recv() in the responderThread */ |
204 | 0 | shutdown(fd, SHUT_RDWR); |
205 | 0 | close(fd); |
206 | 0 | fd = -1; |
207 | 0 | } |
208 | 0 | } |
209 | 0 | } |
210 | |
|
211 | 0 | if (connected) { |
212 | 0 | lock.unlock(); |
213 | 0 | d_connectedWait.notify_all(); |
214 | 0 | if (!initialAttempt) { |
215 | | /* we need to be careful not to start this |
216 | | thread too soon, as the creation should only |
217 | | happen after the configuration has been parsed */ |
218 | 0 | start(); |
219 | 0 | } |
220 | 0 | } |
221 | |
|
222 | 0 | return connected; |
223 | 0 | } |
224 | | |
225 | | void DownstreamState::waitUntilConnected() |
226 | 0 | { |
227 | 0 | if (d_stopped) { |
228 | 0 | return; |
229 | 0 | } |
230 | 0 | if (connected) { |
231 | 0 | return; |
232 | 0 | } |
233 | 0 | { |
234 | 0 | std::unique_lock<std::mutex> lock(connectLock); |
235 | 0 | d_connectedWait.wait(lock, [this] { |
236 | 0 | return connected.load(); |
237 | 0 | }); |
238 | 0 | } |
239 | 0 | } |
240 | | |
241 | | void DownstreamState::stop() |
242 | 0 | { |
243 | 0 | if (d_stopped) { |
244 | 0 | return; |
245 | 0 | } |
246 | 0 | d_stopped = true; |
247 | |
|
248 | 0 | { |
249 | 0 | auto tlock = std::scoped_lock(connectLock); |
250 | 0 | auto slock = mplexer.lock(); |
251 | |
|
252 | 0 | for (auto& fd : sockets) { |
253 | 0 | if (fd != -1) { |
254 | | /* shutdown() is needed to wake up recv() in the responderThread */ |
255 | 0 | shutdown(fd, SHUT_RDWR); |
256 | 0 | } |
257 | 0 | } |
258 | 0 | } |
259 | 0 | } |
260 | | |
261 | | void DownstreamState::hash() |
262 | 0 | { |
263 | 0 | const auto hashPerturbation = dnsdist::configuration::getImmutableConfiguration().d_hashPerturbation; |
264 | 0 | VERBOSESLOG(infolog("Computing hashes for id=%s and weight=%d, hash_perturbation=%d", *d_config.id, d_config.d_weight, hashPerturbation), |
265 | 0 | dnsdist::logging::getTopLogger("backend")->info(Logr::Info, "Computing hashes for backend", "backend.id", Logging::Loggable(*d_config.id), "backend.weight", Logging::Loggable(d_config.d_weight), "backend.hash_perturbation", Logging::Loggable(hashPerturbation))); |
266 | |
|
267 | 0 | auto weight = d_config.d_weight; |
268 | 0 | auto idStr = boost::str(boost::format("%s") % *d_config.id); |
269 | 0 | auto lockedHashes = hashes.write_lock(); |
270 | 0 | lockedHashes->clear(); |
271 | 0 | lockedHashes->reserve(weight); |
272 | 0 | while (weight > 0) { |
273 | 0 | std::string uuid = boost::str(boost::format("%s-%d") % idStr % weight); |
274 | | // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): sorry, it's the burtle API |
275 | 0 | unsigned int wshash = burtleCI(reinterpret_cast<const unsigned char*>(uuid.c_str()), uuid.size(), hashPerturbation); |
276 | 0 | lockedHashes->push_back(wshash); |
277 | 0 | --weight; |
278 | 0 | } |
279 | 0 | std::sort(lockedHashes->begin(), lockedHashes->end()); |
280 | 0 | hashesComputed = true; |
281 | 0 | } |
282 | | |
283 | | void DownstreamState::setId(const boost::uuids::uuid& newId) |
284 | 0 | { |
285 | 0 | d_config.id = newId; |
286 | | // compute hashes only if already done |
287 | 0 | if (hashesComputed) { |
288 | 0 | hash(); |
289 | 0 | } |
290 | 0 | } |
291 | | |
292 | | void DownstreamState::setWeight(int newWeight) |
293 | 0 | { |
294 | 0 | if (newWeight < 1) { |
295 | 0 | SLOG(errlog("Error setting server's weight: downstream weight value must be greater than 0."), |
296 | 0 | getLogger()->info(Logr::Error, "Error setting server's weight: downstream weight value must be greater than 0", "backend.weight", Logging::Loggable(newWeight))); |
297 | 0 | return; |
298 | 0 | } |
299 | | |
300 | 0 | d_config.d_weight = newWeight; |
301 | |
|
302 | 0 | if (hashesComputed) { |
303 | 0 | hash(); |
304 | 0 | } |
305 | 0 | } |
306 | | |
307 | | DownstreamState::DownstreamState(DownstreamState::Config&& config, std::shared_ptr<TLSCtx> tlsCtx, bool connect) : |
308 | 0 | d_config(std::move(config)), d_tlsCtx(std::move(tlsCtx)) |
309 | 0 | { |
310 | 0 | threadStarted.clear(); |
311 | |
|
312 | 0 | if (d_config.d_qpsLimit > 0) { |
313 | 0 | d_qpsLimiter = QPSLimiter(d_config.d_qpsLimit, d_config.d_qpsLimit); |
314 | 0 | } |
315 | |
|
316 | 0 | if (d_config.id) { |
317 | 0 | setId(*d_config.id); |
318 | 0 | } |
319 | 0 | else { |
320 | 0 | d_config.id = getUniqueID(); |
321 | 0 | } |
322 | |
|
323 | 0 | if (d_config.d_weight > 0) { |
324 | 0 | setWeight(d_config.d_weight); |
325 | 0 | } |
326 | |
|
327 | 0 | if (d_config.d_availability == Availability::Auto && d_config.d_healthCheckMode == HealthCheckMode::Lazy && d_config.d_lazyHealthCheckSampleSize > 0) { |
328 | 0 | d_lazyHealthCheckStats.lock()->d_lastResults.set_capacity(d_config.d_lazyHealthCheckSampleSize); |
329 | 0 | setUpStatus(true); |
330 | 0 | } |
331 | |
|
332 | 0 | setName(d_config.name); |
333 | |
|
334 | 0 | if (d_tlsCtx && !d_config.d_dohPath.empty()) { |
335 | | #ifdef HAVE_NGHTTP2 |
336 | | auto outgoingDoHWorkerThreads = dnsdist::configuration::getImmutableConfiguration().d_outgoingDoHWorkers; |
337 | | if (dnsdist::configuration::isImmutableConfigurationDone() && outgoingDoHWorkerThreads && *outgoingDoHWorkerThreads == 0) { |
338 | | throw std::runtime_error("Error: setOutgoingDoHWorkerThreads() is set to 0 so no outgoing DoH worker thread is available to serve queries"); |
339 | | } |
340 | | |
341 | | if (!dnsdist::configuration::isImmutableConfigurationDone() && (!outgoingDoHWorkerThreads || *outgoingDoHWorkerThreads == 0)) { |
342 | | dnsdist::configuration::updateImmutableConfiguration([](dnsdist::configuration::ImmutableConfiguration& immutableConfig) { |
343 | | immutableConfig.d_outgoingDoHWorkers = 1; |
344 | | }); |
345 | | } |
346 | | #endif /* HAVE_NGHTTP2 */ |
347 | 0 | } |
348 | |
|
349 | 0 | if (connect && !isTCPOnly()) { |
350 | 0 | if (!IsAnyAddress(d_config.remote)) { |
351 | 0 | connectUDPSockets(); |
352 | 0 | } |
353 | 0 | } |
354 | |
|
355 | 0 | sw.start(); |
356 | 0 | } |
357 | | |
358 | | void DownstreamState::start() |
359 | 0 | { |
360 | 0 | if (connected && !threadStarted.test_and_set()) { |
361 | | #ifdef HAVE_XSK |
362 | | for (auto& xskInfo : d_xskInfos) { |
363 | | auto xskResponderThread = std::thread(dnsdist::xsk::XskResponderThread, shared_from_this(), xskInfo); |
364 | | if (!d_config.d_cpus.empty()) { |
365 | | mapThreadToCPUList(xskResponderThread.native_handle(), d_config.d_cpus); |
366 | | } |
367 | | xskResponderThread.detach(); |
368 | | } |
369 | | #endif /* HAVE_XSK */ |
370 | |
|
371 | 0 | auto tid = std::thread(dnsdist::udp::responderThread, shared_from_this()); |
372 | 0 | if (!d_config.d_cpus.empty()) { |
373 | 0 | mapThreadToCPUList(tid.native_handle(), d_config.d_cpus); |
374 | 0 | } |
375 | 0 | tid.detach(); |
376 | 0 | } |
377 | 0 | } |
378 | | |
379 | | void DownstreamState::connectUDPSockets() |
380 | 0 | { |
381 | 0 | const auto& config = dnsdist::configuration::getImmutableConfiguration(); |
382 | 0 | if (config.d_randomizeIDsToBackend) { |
383 | 0 | idStates.clear(); |
384 | 0 | } |
385 | 0 | else { |
386 | 0 | const auto maxUDPOutstanding = d_config.d_maxUDPOutstanding > 0 ? d_config.d_maxUDPOutstanding : config.d_maxUDPOutstanding; |
387 | 0 | idStates.resize(maxUDPOutstanding); |
388 | 0 | } |
389 | 0 | sockets.resize(d_config.d_numberOfSockets); |
390 | |
|
391 | 0 | if (sockets.size() > 1) { |
392 | 0 | *(mplexer.lock()) = std::unique_ptr<FDMultiplexer>(FDMultiplexer::getMultiplexerSilent(sockets.size())); |
393 | 0 | } |
394 | |
|
395 | 0 | for (auto& fd : sockets) { |
396 | 0 | fd = -1; |
397 | 0 | } |
398 | |
|
399 | 0 | reconnect(true); |
400 | 0 | } |
401 | | |
402 | | DownstreamState::~DownstreamState() |
403 | 0 | { |
404 | 0 | for (auto& fd : sockets) { |
405 | 0 | if (fd >= 0) { |
406 | 0 | close(fd); |
407 | 0 | fd = -1; |
408 | 0 | } |
409 | 0 | } |
410 | 0 | } |
411 | | |
412 | | void DownstreamState::incCurrentConnectionsCount() |
413 | 0 | { |
414 | 0 | auto currentConnectionsCount = ++tcpCurrentConnections; |
415 | 0 | if (currentConnectionsCount > tcpMaxConcurrentConnections) { |
416 | 0 | tcpMaxConcurrentConnections.store(currentConnectionsCount); |
417 | 0 | } |
418 | 0 | } |
419 | | |
420 | | int DownstreamState::pickSocketForSending() |
421 | 0 | { |
422 | 0 | size_t numberOfSockets = sockets.size(); |
423 | 0 | if (numberOfSockets == 1) { |
424 | 0 | return sockets[0]; |
425 | 0 | } |
426 | | |
427 | 0 | size_t idx{0}; |
428 | 0 | if (dnsdist::configuration::getImmutableConfiguration().d_randomizeUDPSocketsToBackend) { |
429 | 0 | idx = dnsdist::getRandomValue(numberOfSockets); |
430 | 0 | } |
431 | 0 | else { |
432 | 0 | idx = socketsOffset++; |
433 | 0 | } |
434 | |
|
435 | 0 | return sockets[idx % numberOfSockets]; |
436 | 0 | } |
437 | | |
438 | | void DownstreamState::pickSocketsReadyForReceiving(std::vector<int>& ready) |
439 | 0 | { |
440 | 0 | ready.clear(); |
441 | |
|
442 | 0 | if (sockets.size() == 1) { |
443 | 0 | ready.push_back(sockets[0]); |
444 | 0 | return; |
445 | 0 | } |
446 | | |
447 | 0 | (*mplexer.lock())->getAvailableFDs(ready, 1000); |
448 | 0 | } |
449 | | |
450 | | static bool isIDSExpired(const IDState& ids, uint8_t udpTimeout) |
451 | 0 | { |
452 | 0 | auto age = ids.age.load(); |
453 | 0 | return age > udpTimeout; |
454 | 0 | } |
455 | | |
456 | | void DownstreamState::handleUDPTimeout(IDState& ids) |
457 | 0 | { |
458 | 0 | ids.age = 0; |
459 | 0 | ids.inUse = false; |
460 | 0 | ++reuseds; |
461 | 0 | --outstanding; |
462 | 0 | ++dnsdist::metrics::g_stats.downstreamTimeouts; // this is an 'actively' discovered timeout |
463 | 0 | VERBOSESLOG(infolog("Had a downstream timeout from %s (%s) for query for %s|%s from %s", |
464 | 0 | d_config.remote.toStringWithPort(), getName(), |
465 | 0 | ids.internal.qname.toLogString(), QType(ids.internal.qtype).toString(), ids.internal.origRemote.toStringWithPort()), |
466 | 0 | getLogger()->info(Logr::Info, "Had a downstream timeout", "dns.question.name", Logging::Loggable(ids.internal.qname), "dns.question.type", Logging::Loggable(ids.internal.qtype), "dns.question.class", Logging::Loggable(ids.internal.qclass), "dns.question.id", Logging::Loggable(ntohs(ids.internal.origID)), "client.address", Logging::Loggable(ids.internal.origRemote))); |
467 | |
|
468 | 0 | const auto& chains = dnsdist::configuration::getCurrentRuntimeConfiguration().d_ruleChains; |
469 | 0 | const auto& timeoutRespRules = dnsdist::rules::getResponseRuleChain(chains, dnsdist::rules::ResponseRuleChain::TimeoutResponseRules); |
470 | 0 | auto sender = ids.internal.du == nullptr ? nullptr : ids.internal.du->getQuerySender(); |
471 | 0 | if (!handleTimeoutResponseRules(timeoutRespRules, ids.internal, shared_from_this(), sender)) { |
472 | 0 | DOHUnitInterface::handleTimeout(std::move(ids.internal.du)); |
473 | 0 | } |
474 | |
|
475 | 0 | if (g_rings.shouldRecordResponses()) { |
476 | 0 | timespec now{}; |
477 | 0 | gettime(&now); |
478 | |
|
479 | 0 | dnsheader fake{}; |
480 | 0 | memset(&fake, 0, sizeof(fake)); |
481 | 0 | fake.id = ids.internal.origID; |
482 | 0 | uint16_t* flags = getFlagsFromDNSHeader(&fake); |
483 | 0 | *flags = ids.internal.origFlags; |
484 | |
|
485 | 0 | g_rings.insertResponse(now, ids.internal.origRemote, std::move(ids.internal.qname), ids.internal.qtype, std::numeric_limits<unsigned int>::max(), 0, fake, d_config.remote, getProtocol()); |
486 | 0 | } |
487 | |
|
488 | 0 | reportTimeoutOrError(); |
489 | 0 | } |
490 | | |
491 | | void DownstreamState::reportResponse(uint8_t rcode) |
492 | 0 | { |
493 | 0 | if (d_config.d_availability == Availability::Auto && d_config.d_healthCheckMode == HealthCheckMode::Lazy && d_config.d_lazyHealthCheckSampleSize > 0) { |
494 | 0 | bool failure = d_config.d_lazyHealthCheckMode == LazyHealthCheckMode::TimeoutOrServFail ? rcode == RCode::ServFail : false; |
495 | 0 | d_lazyHealthCheckStats.lock()->d_lastResults.push_back(failure); |
496 | 0 | } |
497 | 0 | } |
498 | | |
499 | | void DownstreamState::reportTimeoutOrError() |
500 | 0 | { |
501 | 0 | if (d_config.d_availability == Availability::Auto && d_config.d_healthCheckMode == HealthCheckMode::Lazy && d_config.d_lazyHealthCheckSampleSize > 0) { |
502 | 0 | d_lazyHealthCheckStats.lock()->d_lastResults.push_back(true); |
503 | 0 | } |
504 | 0 | } |
505 | | |
506 | | void DownstreamState::handleUDPTimeouts() |
507 | 0 | { |
508 | 0 | if (getProtocol() != dnsdist::Protocol::DoUDP) { |
509 | 0 | return; |
510 | 0 | } |
511 | | |
512 | 0 | const auto& config = dnsdist::configuration::getImmutableConfiguration(); |
513 | 0 | const auto udpTimeout = d_config.udpTimeout > 0 ? d_config.udpTimeout : config.d_udpTimeout; |
514 | 0 | if (config.d_randomizeIDsToBackend) { |
515 | 0 | auto map = d_idStatesMap.lock(); |
516 | 0 | for (auto it = map->begin(); it != map->end();) { |
517 | 0 | auto& ids = it->second; |
518 | 0 | if (isIDSExpired(ids, udpTimeout)) { |
519 | 0 | handleUDPTimeout(ids); |
520 | 0 | it = map->erase(it); |
521 | 0 | continue; |
522 | 0 | } |
523 | 0 | ++ids.age; |
524 | 0 | ++it; |
525 | 0 | } |
526 | 0 | } |
527 | 0 | else { |
528 | 0 | if (outstanding.load() > 0) { |
529 | 0 | for (IDState& ids : idStates) { |
530 | 0 | if (!ids.isInUse()) { |
531 | 0 | continue; |
532 | 0 | } |
533 | 0 | if (!isIDSExpired(ids, udpTimeout)) { |
534 | 0 | ++ids.age; |
535 | 0 | continue; |
536 | 0 | } |
537 | 0 | auto guard = ids.acquire(); |
538 | 0 | if (!guard) { |
539 | 0 | continue; |
540 | 0 | } |
541 | | /* check again, now that we have locked this state */ |
542 | 0 | if (ids.isInUse() && isIDSExpired(ids, udpTimeout)) { |
543 | 0 | handleUDPTimeout(ids); |
544 | 0 | } |
545 | 0 | } |
546 | 0 | } |
547 | 0 | } |
548 | 0 | } |
549 | | |
550 | | uint16_t DownstreamState::saveState(InternalQueryState&& state) |
551 | 0 | { |
552 | 0 | const auto& config = dnsdist::configuration::getImmutableConfiguration(); |
553 | 0 | if (config.d_randomizeIDsToBackend) { |
554 | | /* if the state is already in use we will retry, |
555 | | up to 5 five times. The last selected one is used |
556 | | even if it was already in use */ |
557 | 0 | size_t remainingAttempts = 5; |
558 | 0 | auto map = d_idStatesMap.lock(); |
559 | |
|
560 | 0 | do { |
561 | 0 | uint16_t selectedID = dnsdist::getRandomValue(std::numeric_limits<uint16_t>::max()); |
562 | 0 | auto [it, inserted] = map->emplace(selectedID, IDState()); |
563 | |
|
564 | 0 | if (!inserted) { |
565 | 0 | remainingAttempts--; |
566 | 0 | if (remainingAttempts > 0) { |
567 | 0 | continue; |
568 | 0 | } |
569 | | |
570 | 0 | auto oldDU = std::move(it->second.internal.du); |
571 | 0 | ++reuseds; |
572 | 0 | ++dnsdist::metrics::g_stats.downstreamTimeouts; |
573 | 0 | DOHUnitInterface::handleTimeout(std::move(oldDU)); |
574 | 0 | } |
575 | 0 | else { |
576 | 0 | ++outstanding; |
577 | 0 | } |
578 | | |
579 | 0 | it->second.internal = std::move(state); |
580 | 0 | it->second.age.store(0); |
581 | |
|
582 | 0 | return it->first; |
583 | 0 | } while (true); |
584 | 0 | } |
585 | | |
586 | 0 | do { |
587 | 0 | uint16_t selectedID = (idOffset++) % idStates.size(); |
588 | 0 | IDState& ids = idStates.at(selectedID); |
589 | 0 | auto guard = ids.acquire(); |
590 | 0 | if (!guard) { |
591 | 0 | continue; |
592 | 0 | } |
593 | 0 | if (ids.isInUse()) { |
594 | | /* we are reusing a state, no change in outstanding but if there was an existing DOHUnit we need |
595 | | to handle it because it's about to be overwritten. */ |
596 | 0 | auto oldDU = std::move(ids.internal.du); |
597 | 0 | ++reuseds; |
598 | 0 | ++dnsdist::metrics::g_stats.downstreamTimeouts; |
599 | 0 | DOHUnitInterface::handleTimeout(std::move(oldDU)); |
600 | 0 | } |
601 | 0 | else { |
602 | 0 | ++outstanding; |
603 | 0 | } |
604 | 0 | ids.internal = std::move(state); |
605 | 0 | ids.age.store(0); |
606 | 0 | ids.inUse = true; |
607 | 0 | return selectedID; |
608 | 0 | } while (true); |
609 | 0 | } |
610 | | |
611 | | void DownstreamState::restoreState(uint16_t id, InternalQueryState&& state) |
612 | 0 | { |
613 | 0 | const auto& config = dnsdist::configuration::getImmutableConfiguration(); |
614 | 0 | if (config.d_randomizeIDsToBackend) { |
615 | 0 | auto map = d_idStatesMap.lock(); |
616 | |
|
617 | 0 | auto [it, inserted] = map->emplace(id, IDState()); |
618 | 0 | if (!inserted) { |
619 | | /* already used */ |
620 | 0 | ++reuseds; |
621 | 0 | ++dnsdist::metrics::g_stats.downstreamTimeouts; |
622 | 0 | DOHUnitInterface::handleTimeout(std::move(state.du)); |
623 | 0 | } |
624 | 0 | else { |
625 | 0 | it->second.internal = std::move(state); |
626 | 0 | ++outstanding; |
627 | 0 | } |
628 | 0 | return; |
629 | 0 | } |
630 | | |
631 | 0 | auto& ids = idStates.at(id); |
632 | 0 | auto guard = ids.acquire(); |
633 | 0 | if (!guard) { |
634 | | /* already used */ |
635 | 0 | ++reuseds; |
636 | 0 | ++dnsdist::metrics::g_stats.downstreamTimeouts; |
637 | 0 | DOHUnitInterface::handleTimeout(std::move(state.du)); |
638 | 0 | return; |
639 | 0 | } |
640 | 0 | if (ids.isInUse()) { |
641 | | /* already used */ |
642 | 0 | ++reuseds; |
643 | 0 | ++dnsdist::metrics::g_stats.downstreamTimeouts; |
644 | 0 | DOHUnitInterface::handleTimeout(std::move(state.du)); |
645 | 0 | return; |
646 | 0 | } |
647 | 0 | ids.internal = std::move(state); |
648 | 0 | ids.inUse = true; |
649 | 0 | ++outstanding; |
650 | 0 | } |
651 | | |
652 | | std::optional<InternalQueryState> DownstreamState::getState(uint16_t id) |
653 | 0 | { |
654 | 0 | std::optional<InternalQueryState> result = std::nullopt; |
655 | 0 | const auto& config = dnsdist::configuration::getImmutableConfiguration(); |
656 | 0 | if (config.d_randomizeIDsToBackend) { |
657 | 0 | auto map = d_idStatesMap.lock(); |
658 | |
|
659 | 0 | auto it = map->find(id); |
660 | 0 | if (it == map->end()) { |
661 | 0 | return result; |
662 | 0 | } |
663 | | |
664 | 0 | result = std::move(it->second.internal); |
665 | 0 | map->erase(it); |
666 | 0 | --outstanding; |
667 | 0 | return result; |
668 | 0 | } |
669 | | |
670 | 0 | if (id >= idStates.size()) { |
671 | 0 | return result; |
672 | 0 | } |
673 | | |
674 | 0 | auto& ids = idStates.at(id); |
675 | 0 | auto guard = ids.acquire(); |
676 | 0 | if (!guard) { |
677 | 0 | return result; |
678 | 0 | } |
679 | | |
680 | 0 | if (ids.isInUse()) { |
681 | 0 | result = std::move(ids.internal); |
682 | 0 | --outstanding; |
683 | 0 | } |
684 | 0 | ids.inUse = false; |
685 | 0 | return result; |
686 | 0 | } |
687 | | |
688 | | bool DownstreamState::healthCheckRequired(std::optional<time_t> currentTime) |
689 | 0 | { |
690 | 0 | if (d_config.d_availability != DownstreamState::Availability::Auto) { |
691 | 0 | return false; |
692 | 0 | } |
693 | | |
694 | 0 | if (d_config.d_healthCheckMode == DownstreamState::HealthCheckMode::Lazy) { |
695 | 0 | auto stats = d_lazyHealthCheckStats.lock(); |
696 | 0 | if (stats->d_status == LazyHealthCheckStats::LazyStatus::PotentialFailure) { |
697 | 0 | VERBOSESLOG(infolog("Sending health-check query for %s which is still in the Potential Failure state", getNameWithAddr()), |
698 | 0 | getLogger()->info(Logr::Info, "Sending health-check query for a backend which is still in the Potential Failure state")); |
699 | 0 | return true; |
700 | 0 | } |
701 | 0 | if (stats->d_status == LazyHealthCheckStats::LazyStatus::Failed) { |
702 | 0 | auto now = currentTime ? *currentTime : time(nullptr); |
703 | 0 | if (stats->d_nextCheck <= now) { |
704 | | /* we update the next check time here because the check might time out, |
705 | | and we do not want to send a second check during that time unless |
706 | | the timer is actually very short */ |
707 | 0 | VERBOSESLOG(infolog("Sending health-check query for %s which is still in the Failed state", getNameWithAddr()), |
708 | 0 | getLogger()->info(Logr::Info, "Sending health-check query for backend which is still in the Failed state")); |
709 | 0 | updateNextLazyHealthCheck(*stats, true, now); |
710 | 0 | return true; |
711 | 0 | } |
712 | 0 | return false; |
713 | 0 | } |
714 | 0 | if (stats->d_status == LazyHealthCheckStats::LazyStatus::Healthy) { |
715 | 0 | auto& lastResults = stats->d_lastResults; |
716 | 0 | size_t totalCount = lastResults.size(); |
717 | 0 | if (totalCount < d_config.d_lazyHealthCheckMinSampleCount) { |
718 | 0 | return false; |
719 | 0 | } |
720 | | |
721 | 0 | size_t failures = 0; |
722 | 0 | for (const auto& result : lastResults) { |
723 | 0 | if (result) { |
724 | 0 | ++failures; |
725 | 0 | } |
726 | 0 | } |
727 | |
|
728 | 0 | const auto maxFailureRate = static_cast<float>(d_config.d_lazyHealthCheckThreshold); |
729 | 0 | auto current = (100.0 * failures) / totalCount; |
730 | 0 | if (current >= maxFailureRate) { |
731 | 0 | lastResults.clear(); |
732 | 0 | VERBOSESLOG(infolog("Backend %s reached the lazy health-check threshold (%f%% out of %f%%, looking at sample of %d items with %d failures), moving to Potential Failure state", getNameWithAddr(), current, maxFailureRate, totalCount, failures), |
733 | 0 | getLogger()->info(Logr::Info, "Backend reached the lazy health-check threshold, moving to Potential failure state", "backend.health_check.current_failure_rate", Logging::Loggable(current), "backend.health_check.max_failure_rate", Logging::Loggable(maxFailureRate), "backend.health_check.samples", Logging::Loggable(totalCount), "backend.health_check.failures_count", Logging::Loggable(failures))); |
734 | 0 | stats->d_status = LazyHealthCheckStats::LazyStatus::PotentialFailure; |
735 | 0 | consecutiveSuccessfulChecks = 0; |
736 | | /* we update the next check time here because the check might time out, |
737 | | and we do not want to send a second check during that time unless |
738 | | the timer is actually very short */ |
739 | 0 | updateNextLazyHealthCheck(*stats, true); |
740 | 0 | return true; |
741 | 0 | } |
742 | 0 | } |
743 | | |
744 | 0 | return false; |
745 | 0 | } |
746 | 0 | if (d_config.d_healthCheckMode == DownstreamState::HealthCheckMode::Active) { |
747 | |
|
748 | 0 | if (d_nextCheck > 1) { |
749 | 0 | --d_nextCheck; |
750 | 0 | return false; |
751 | 0 | } |
752 | | |
753 | 0 | d_nextCheck = d_config.checkInterval; |
754 | 0 | return true; |
755 | 0 | } |
756 | | |
757 | 0 | return false; |
758 | 0 | } |
759 | | |
760 | | time_t DownstreamState::getNextLazyHealthCheck() |
761 | 0 | { |
762 | 0 | auto stats = d_lazyHealthCheckStats.lock(); |
763 | 0 | return stats->d_nextCheck; |
764 | 0 | } |
765 | | |
766 | | void DownstreamState::updateNextLazyHealthCheck(LazyHealthCheckStats& stats, bool checkScheduled, std::optional<time_t> currentTime) |
767 | 0 | { |
768 | 0 | auto now = currentTime ? *currentTime : time(nullptr); |
769 | 0 | if (d_config.d_lazyHealthCheckUseExponentialBackOff) { |
770 | 0 | if (stats.d_status == DownstreamState::LazyHealthCheckStats::LazyStatus::PotentialFailure) { |
771 | | /* we are still in the "up" state, we need to send the next query quickly to |
772 | | determine if the backend is really down */ |
773 | 0 | stats.d_nextCheck = now + d_config.checkInterval; |
774 | 0 | VERBOSESLOG(infolog("Backend %s is in potential failure state, next check in %d seconds", getNameWithAddr(), d_config.checkInterval.load()), |
775 | 0 | getLogger()->info(Logr::Info, "Backend is in potential failure state", "backend.health_check.next_check", Logging::Loggable(d_config.checkInterval.load()))); |
776 | 0 | } |
777 | 0 | else if (consecutiveSuccessfulChecks > 0) { |
778 | | /* we are in 'Failed' state, but just had one (or more) successful check, |
779 | | so we want the next one to happen quite quickly as the backend might |
780 | | be available again. */ |
781 | 0 | stats.d_nextCheck = now + d_config.d_lazyHealthCheckFailedInterval; |
782 | 0 | if (!checkScheduled) { |
783 | 0 | VERBOSESLOG(infolog("Backend %s is in failed state but had %d consecutive successful checks, next check in %d seconds", getNameWithAddr(), std::to_string(consecutiveSuccessfulChecks), d_config.d_lazyHealthCheckFailedInterval), |
784 | 0 | getLogger()->info(Logr::Info, "Backend is in failed state but had a successful check", "backend.health_check.consecutive_successful_checks", Logging::Loggable(consecutiveSuccessfulChecks), "backend.health_check.next_check", Logging::Loggable(d_config.d_lazyHealthCheckFailedInterval))); |
785 | 0 | } |
786 | 0 | } |
787 | 0 | else { |
788 | 0 | uint16_t failedTests = currentCheckFailures; |
789 | 0 | if (checkScheduled) { |
790 | | /* we are planning the check after that one, which will only |
791 | | occur if there is a failure */ |
792 | 0 | failedTests++; |
793 | 0 | } |
794 | |
|
795 | 0 | time_t backOff = d_config.d_lazyHealthCheckMaxBackOff; |
796 | 0 | const ExponentialBackOffTimer backOffTimer(d_config.d_lazyHealthCheckMaxBackOff); |
797 | 0 | auto backOffCoeffTmp = backOffTimer.get(failedTests - 1); |
798 | | /* backOffCoeffTmp cannot be higher than d_config.d_lazyHealthCheckMaxBackOff */ |
799 | 0 | const auto backOffCoeff = static_cast<time_t>(backOffCoeffTmp); |
800 | 0 | if ((std::numeric_limits<time_t>::max() / d_config.d_lazyHealthCheckFailedInterval) >= backOffCoeff) { |
801 | 0 | backOff = d_config.d_lazyHealthCheckFailedInterval * backOffCoeff; |
802 | 0 | if (backOff > d_config.d_lazyHealthCheckMaxBackOff || (std::numeric_limits<time_t>::max() - now) <= backOff) { |
803 | 0 | backOff = d_config.d_lazyHealthCheckMaxBackOff; |
804 | 0 | } |
805 | 0 | } |
806 | |
|
807 | 0 | stats.d_nextCheck = now + backOff; |
808 | 0 | VERBOSESLOG(infolog("Backend %s is in failed state and has failed %d consecutive checks, next check in %d seconds", getNameWithAddr(), failedTests, backOff), |
809 | 0 | getLogger()->info(Logr::Info, "Backend is in failed state and has failed its last check", "backend.health_check.consecutive_failed_checks", Logging::Loggable(failedTests), "backend.health_check.next_check", Logging::Loggable(backOff))); |
810 | 0 | } |
811 | 0 | } |
812 | 0 | else { |
813 | 0 | stats.d_nextCheck = now + d_config.d_lazyHealthCheckFailedInterval; |
814 | 0 | VERBOSESLOG(infolog("Backend %s is in %s state, next check in %d seconds", getNameWithAddr(), (stats.d_status == DownstreamState::LazyHealthCheckStats::LazyStatus::PotentialFailure ? "potential failure" : "failed"), d_config.d_lazyHealthCheckFailedInterval), |
815 | 0 | getLogger()->info(Logr::Info, "Scheduling the next lazy check for this backend", "state", Logging::Loggable(stats.d_status == DownstreamState::LazyHealthCheckStats::LazyStatus::PotentialFailure ? "potential failure" : "failed"), "backend.health_check.next_check", Logging::Loggable(d_config.d_lazyHealthCheckFailedInterval))); |
816 | 0 | } |
817 | 0 | } |
818 | | |
819 | | void DownstreamState::submitHealthCheckResult(bool initial, bool newResult) |
820 | 0 | { |
821 | 0 | if (!newResult) { |
822 | 0 | ++d_healthCheckMetrics.d_failures; |
823 | 0 | } |
824 | |
|
825 | 0 | if (initial) { |
826 | | /* if this is the initial health-check, at startup, we do not care |
827 | | about the minimum number of failed/successful health-checks */ |
828 | 0 | if (!IsAnyAddress(d_config.remote)) { |
829 | 0 | SLOG(infolog("Marking downstream %s as '%s'", getNameWithAddr(), newResult ? "up" : "down"), |
830 | 0 | getLogger()->info(Logr::Info, "Setting initial status for backend", "backend.health_check.status", Logging::Loggable(newResult ? "up" : "down"))); |
831 | 0 | } |
832 | 0 | setUpStatus(newResult); |
833 | 0 | if (newResult == false) { |
834 | 0 | currentCheckFailures++; |
835 | 0 | if (d_config.d_healthCheckMode == DownstreamState::HealthCheckMode::Lazy) { |
836 | 0 | auto stats = d_lazyHealthCheckStats.lock(); |
837 | 0 | stats->d_status = LazyHealthCheckStats::LazyStatus::Failed; |
838 | 0 | updateNextLazyHealthCheck(*stats, false); |
839 | 0 | } |
840 | 0 | } |
841 | 0 | handleServerStateChange(getNameWithAddr(), newResult); |
842 | 0 | return; |
843 | 0 | } |
844 | | |
845 | 0 | bool newState = newResult; |
846 | |
|
847 | 0 | if (newResult) { |
848 | | /* check succeeded */ |
849 | 0 | currentCheckFailures = 0; |
850 | 0 | consecutiveSuccessfulChecks++; |
851 | |
|
852 | 0 | if (!upStatus.load(std::memory_order_relaxed)) { |
853 | | /* we were previously marked as "down" and had a successful health-check, |
854 | | let's see if this is enough to move to the "up" state or if we need |
855 | | more successful health-checks for that */ |
856 | 0 | if (consecutiveSuccessfulChecks < d_config.minRiseSuccesses) { |
857 | | /* we need more than one successful check to rise |
858 | | and we didn't reach the threshold yet, let's stay down */ |
859 | 0 | newState = false; |
860 | |
|
861 | 0 | if (d_config.d_healthCheckMode == DownstreamState::HealthCheckMode::Lazy) { |
862 | 0 | auto stats = d_lazyHealthCheckStats.lock(); |
863 | 0 | updateNextLazyHealthCheck(*stats, false); |
864 | 0 | } |
865 | 0 | } |
866 | 0 | } |
867 | |
|
868 | 0 | if (newState) { |
869 | 0 | if (d_config.d_healthCheckMode == DownstreamState::HealthCheckMode::Lazy) { |
870 | 0 | auto stats = d_lazyHealthCheckStats.lock(); |
871 | 0 | VERBOSESLOG(infolog("Backend %s had %d successful checks, moving to Healthy", getNameWithAddr(), std::to_string(consecutiveSuccessfulChecks)), |
872 | 0 | getLogger()->info(Logr::Info, "Backend had a successful check, moving to Healthy", "backend.health_check.consecutive_successful_checks", Logging::Loggable(consecutiveSuccessfulChecks))); |
873 | 0 | stats->d_status = LazyHealthCheckStats::LazyStatus::Healthy; |
874 | 0 | stats->d_lastResults.clear(); |
875 | 0 | } |
876 | 0 | } |
877 | 0 | } |
878 | 0 | else { |
879 | | /* check failed */ |
880 | 0 | consecutiveSuccessfulChecks = 0; |
881 | |
|
882 | 0 | currentCheckFailures++; |
883 | |
|
884 | 0 | if (upStatus.load(std::memory_order_relaxed)) { |
885 | | /* we were previously marked as "up" and failed a health-check, |
886 | | let's see if this is enough to move to the "down" state or if |
887 | | need more failed checks for that */ |
888 | 0 | if (currentCheckFailures < d_config.maxCheckFailures) { |
889 | | /* we need more than one failure to be marked as down, |
890 | | and we did not reach the threshold yet, let's stay up */ |
891 | 0 | newState = true; |
892 | 0 | } |
893 | 0 | else if (d_config.d_healthCheckMode == DownstreamState::HealthCheckMode::Lazy) { |
894 | 0 | auto stats = d_lazyHealthCheckStats.lock(); |
895 | 0 | VERBOSESLOG(infolog("Backend %s failed its health-check, moving from Potential failure to Failed", getNameWithAddr()), |
896 | 0 | getLogger()->info(Logr::Info, "Backend failed its health-check, moving from Potential failure to Failed")); |
897 | 0 | stats->d_status = LazyHealthCheckStats::LazyStatus::Failed; |
898 | 0 | currentCheckFailures = 1; |
899 | 0 | updateNextLazyHealthCheck(*stats, false); |
900 | 0 | } |
901 | 0 | } |
902 | 0 | } |
903 | |
|
904 | 0 | if (newState != upStatus.load(std::memory_order_relaxed)) { |
905 | | /* we are actually moving to a new state */ |
906 | 0 | if (!IsAnyAddress(d_config.remote)) { |
907 | 0 | SLOG(infolog("Marking downstream %s as '%s'", getNameWithAddr(), newState ? "up" : "down"), |
908 | 0 | getLogger()->info(Logr::Info, "Updating status for backend", "backend.health_check.status", Logging::Loggable(newState ? "up" : "down"))); |
909 | 0 | } |
910 | |
|
911 | 0 | if (newState && !isTCPOnly() && (!connected || d_config.reconnectOnUp)) { |
912 | 0 | newState = reconnect(); |
913 | 0 | } |
914 | |
|
915 | 0 | setUpStatus(newState); |
916 | 0 | if (g_snmpAgent != nullptr && dnsdist::configuration::getImmutableConfiguration().d_snmpTrapsEnabled) { |
917 | 0 | g_snmpAgent->sendBackendStatusChangeTrap(*this); |
918 | 0 | } |
919 | 0 | handleServerStateChange(getNameWithAddr(), newResult); |
920 | 0 | } |
921 | 0 | } |
922 | | |
923 | | #ifdef HAVE_XSK |
924 | | [[nodiscard]] ComboAddress DownstreamState::pickSourceAddressForSending() |
925 | | { |
926 | | if (!connected) { |
927 | | waitUntilConnected(); |
928 | | } |
929 | | |
930 | | auto addresses = d_socketSourceAddresses.read_lock(); |
931 | | auto numberOfAddresses = addresses->size(); |
932 | | if (numberOfAddresses == 0) { |
933 | | throw std::runtime_error("No source address available for sending XSK data to backend " + getNameWithAddr()); |
934 | | } |
935 | | size_t idx = dnsdist::getRandomValue(numberOfAddresses); |
936 | | return (*addresses)[idx % numberOfAddresses]; |
937 | | } |
938 | | |
939 | | void DownstreamState::registerXsk(std::vector<std::shared_ptr<XskSocket>>& xsks) |
940 | | { |
941 | | d_xskSockets = xsks; |
942 | | |
943 | | if (d_config.sourceAddr.sin4.sin_family == 0 || (IsAnyAddress(d_config.sourceAddr))) { |
944 | | const auto& ifName = xsks.at(0)->getInterfaceName(); |
945 | | auto addresses = getListOfAddressesOfNetworkInterface(ifName); |
946 | | if (addresses.empty()) { |
947 | | throw std::runtime_error("Unable to get source address from interface " + ifName); |
948 | | } |
949 | | |
950 | | if (addresses.size() > 1) { |
951 | | SLOG(warnlog("More than one address configured on interface %s, picking the first one (%s) for XSK. Set the 'source' parameter on 'newServer' if you want to use a different address.", ifName, addresses.at(0).toString()), |
952 | | getLogger()->info(Logr::Warning, "More than one address configured on this interface, picking the first one for XSK. Set the 'source' parameter on 'newServer' if you want to use a different address.", "source.interface", Logging::Loggable(ifName), "backend.address", Logging::Loggable(addresses.at(0)))); |
953 | | } |
954 | | d_config.sourceAddr = addresses.at(0); |
955 | | } |
956 | | d_config.sourceMACAddr = d_xskSockets.at(0)->getSourceMACAddress(); |
957 | | |
958 | | for (auto& xsk : d_xskSockets) { |
959 | | auto xskInfo = XskWorker::create(XskWorker::Type::Bidirectional, xsk->sharedEmptyFrameOffset); |
960 | | d_xskInfos.push_back(xskInfo); |
961 | | xsk->addWorker(xskInfo); |
962 | | } |
963 | | reconnect(false); |
964 | | } |
965 | | #endif /* HAVE_XSK */ |
966 | | |
967 | | bool DownstreamState::parseSourceParameter(const std::string& source, DownstreamState::Config& config) |
968 | 0 | { |
969 | | /* handle source in the following forms: |
970 | | - v4 address ("192.0.2.1") |
971 | | - v6 address ("2001:DB8::1") |
972 | | - interface name ("eth0") |
973 | | - v4 address and interface name ("192.0.2.1@eth0") |
974 | | - v6 address and interface name ("2001:DB8::1@eth0") |
975 | | */ |
976 | 0 | std::string::size_type pos = source.find('@'); |
977 | 0 | if (pos == std::string::npos) { |
978 | | /* no '@', try to parse that as a valid v4/v6 address */ |
979 | 0 | try { |
980 | 0 | config.sourceAddr = ComboAddress(source); |
981 | 0 | return true; |
982 | 0 | } |
983 | 0 | catch (...) { |
984 | 0 | } |
985 | 0 | } |
986 | | |
987 | | /* try to parse as interface name, or v4/v6@itf */ |
988 | 0 | config.sourceItfName = source.substr(pos == std::string::npos ? 0 : pos + 1); |
989 | 0 | unsigned int itfIdx = if_nametoindex(config.sourceItfName.c_str()); |
990 | 0 | if (itfIdx != 0) { |
991 | 0 | if (pos == 0 || pos == std::string::npos) { |
992 | | /* "eth0" or "@eth0" */ |
993 | 0 | config.sourceItf = itfIdx; |
994 | 0 | } |
995 | 0 | else { |
996 | | /* "192.0.2.1@eth0" */ |
997 | 0 | config.sourceAddr = ComboAddress(source.substr(0, pos)); |
998 | 0 | config.sourceItf = itfIdx; |
999 | 0 | } |
1000 | 0 | #ifdef SO_BINDTODEVICE |
1001 | 0 | if (!dnsdist::configuration::isImmutableConfigurationDone()) { |
1002 | | /* we need to retain CAP_NET_RAW to be able to set SO_BINDTODEVICE in the health checks */ |
1003 | 0 | dnsdist::configuration::updateImmutableConfiguration([](dnsdist::configuration::ImmutableConfiguration& currentConfig) { |
1004 | 0 | currentConfig.d_capabilitiesToRetain.insert("CAP_NET_RAW"); |
1005 | 0 | }); |
1006 | 0 | } |
1007 | 0 | #endif |
1008 | 0 | return true; |
1009 | 0 | } |
1010 | | |
1011 | 0 | SLOG(warnlog("Dismissing source %s because '%s' is not a valid interface name", source, config.sourceItfName), |
1012 | 0 | dnsdist::logging::getTopLogger("backend")->info(Logr::Warning, "Dismissing source because the interface name is not valid", "backend.name", Logging::Loggable(config.name), "backend.address", Logging::Loggable(config.remote), "source.address", Logging::Loggable(source), "source.interface", Logging::Loggable(config.sourceItfName))); |
1013 | |
|
1014 | 0 | return false; |
1015 | 0 | } |
1016 | | |
1017 | | bool DownstreamState::parseAvailabilityConfigFromStr(DownstreamState::Config& config, const std::string& str) |
1018 | 0 | { |
1019 | 0 | if (pdns_iequals(str, "auto")) { |
1020 | 0 | config.d_availability = DownstreamState::Availability::Auto; |
1021 | 0 | config.d_healthCheckMode = DownstreamState::HealthCheckMode::Active; |
1022 | 0 | return true; |
1023 | 0 | } |
1024 | 0 | if (pdns_iequals(str, "lazy")) { |
1025 | 0 | config.d_availability = DownstreamState::Availability::Auto; |
1026 | 0 | config.d_healthCheckMode = DownstreamState::HealthCheckMode::Lazy; |
1027 | 0 | return true; |
1028 | 0 | } |
1029 | 0 | if (pdns_iequals(str, "up")) { |
1030 | 0 | config.d_availability = DownstreamState::Availability::Up; |
1031 | 0 | return true; |
1032 | 0 | } |
1033 | 0 | if (pdns_iequals(str, "down")) { |
1034 | 0 | config.d_availability = DownstreamState::Availability::Down; |
1035 | 0 | return true; |
1036 | 0 | } |
1037 | 0 | return false; |
1038 | 0 | } |
1039 | | |
1040 | | unsigned int DownstreamState::getQPSLimit() const |
1041 | 0 | { |
1042 | 0 | return d_qpsLimiter ? d_qpsLimiter->getRate() : 0U; |
1043 | 0 | } |
1044 | | |
1045 | | [[nodiscard]] std::shared_ptr<const Logr::Logger> DownstreamState::getLogger() const |
1046 | 0 | { |
1047 | 0 | return dnsdist::logging::getTopLogger("backend")->withValues("backend.name", Logging::Loggable(getName()), "backend.address", Logging::Loggable(d_config.remote), "backend.protocol", Logging::Loggable(getProtocol())); |
1048 | 0 | } |
1049 | | |
1050 | | bool DownstreamState::canAcceptNewQueries(bool enforceQPS) const |
1051 | 0 | { |
1052 | 0 | if (!isUp()) { |
1053 | 0 | return false; |
1054 | 0 | } |
1055 | | |
1056 | 0 | if (d_config.d_maxOutstandingQueries > 0 && outstanding.load() >= d_config.d_maxOutstandingQueries) { |
1057 | 0 | return false; |
1058 | 0 | } |
1059 | | |
1060 | 0 | if (enforceQPS && d_qpsLimiter && !d_qpsLimiter->checkOnly()) { |
1061 | 0 | return false; |
1062 | 0 | } |
1063 | | |
1064 | 0 | return true; |
1065 | 0 | } |
1066 | | |
1067 | | size_t ServerPool::countServers(bool upOnly) const |
1068 | 0 | { |
1069 | 0 | size_t count = 0; |
1070 | 0 | for (const auto& server : d_servers) { |
1071 | 0 | if (!upOnly || std::get<1>(server)->canAcceptNewQueries(true)) { |
1072 | 0 | count++; |
1073 | 0 | } |
1074 | 0 | } |
1075 | |
|
1076 | 0 | return count; |
1077 | 0 | } |
1078 | | |
1079 | | size_t ServerPool::poolLoad() const |
1080 | 0 | { |
1081 | 0 | size_t load = 0; |
1082 | 0 | for (const auto& server : d_servers) { |
1083 | 0 | size_t serverOutstanding = std::get<1>(server)->outstanding.load(); |
1084 | 0 | load += serverOutstanding; |
1085 | 0 | } |
1086 | 0 | return load; |
1087 | 0 | } |
1088 | | |
1089 | | bool ServerPool::hasAtLeastOneServerAvailable() const |
1090 | 0 | { |
1091 | | // NOLINTNEXTLINE(readability-use-anyofallof): no it's not more readable |
1092 | 0 | for (const auto& server : d_servers) { |
1093 | 0 | if (std::get<1>(server)->canAcceptNewQueries(true)) { |
1094 | 0 | return true; |
1095 | 0 | } |
1096 | 0 | } |
1097 | 0 | return false; |
1098 | 0 | } |
1099 | | |
1100 | | const ServerPolicy::NumberedServerVector& ServerPool::getServers() const |
1101 | 0 | { |
1102 | 0 | return d_servers; |
1103 | 0 | } |
1104 | | |
1105 | | void ServerPool::addServer(std::shared_ptr<DownstreamState>& server) |
1106 | 0 | { |
1107 | 0 | auto count = static_cast<unsigned int>(d_servers.size()); |
1108 | 0 | d_servers.emplace_back(++count, server); |
1109 | | /* we need to reorder based on the server 'order' */ |
1110 | 0 | std::stable_sort(d_servers.begin(), d_servers.end(), [](const std::pair<unsigned int, std::shared_ptr<DownstreamState>>& lhs, const std::pair<unsigned int, std::shared_ptr<DownstreamState>>& rhs) { |
1111 | 0 | return lhs.second->d_config.order < rhs.second->d_config.order; |
1112 | 0 | }); |
1113 | | /* and now we need to renumber for Lua (custom policies) */ |
1114 | 0 | size_t idx = 1; |
1115 | 0 | for (auto& serv : d_servers) { |
1116 | 0 | serv.first = idx++; |
1117 | 0 | } |
1118 | |
|
1119 | 0 | updateConsistency(); |
1120 | 0 | } |
1121 | | |
1122 | | void ServerPool::removeServer(shared_ptr<DownstreamState>& server) |
1123 | 0 | { |
1124 | 0 | size_t idx = 1; |
1125 | 0 | bool found = false; |
1126 | 0 | for (auto it = d_servers.begin(); it != d_servers.end();) { |
1127 | 0 | if (found) { |
1128 | | /* we need to renumber the servers placed |
1129 | | after the removed one, for Lua (custom policies) */ |
1130 | 0 | it->first = idx++; |
1131 | 0 | it++; |
1132 | 0 | } |
1133 | 0 | else if (it->second == server) { |
1134 | 0 | it = d_servers.erase(it); |
1135 | 0 | found = true; |
1136 | 0 | } |
1137 | 0 | else { |
1138 | 0 | idx++; |
1139 | 0 | it++; |
1140 | 0 | } |
1141 | 0 | } |
1142 | |
|
1143 | 0 | if (found && !d_isConsistent) { |
1144 | 0 | updateConsistency(); |
1145 | 0 | } |
1146 | 0 | } |
1147 | | |
1148 | | void ServerPool::updateConsistency() |
1149 | 0 | { |
1150 | 0 | bool consistent{true}; |
1151 | 0 | bool first{true}; |
1152 | 0 | bool useECS{false}; |
1153 | 0 | bool tcpOnly{false}; |
1154 | 0 | bool zeroScope{true}; |
1155 | |
|
1156 | 0 | for (const auto& serverPair : d_servers) { |
1157 | 0 | const auto& server = serverPair.second; |
1158 | 0 | if (first) { |
1159 | 0 | first = false; |
1160 | 0 | useECS = server->d_config.useECS; |
1161 | 0 | tcpOnly = server->isTCPOnly(); |
1162 | 0 | zeroScope = !server->d_config.disableZeroScope; |
1163 | 0 | continue; |
1164 | 0 | } |
1165 | 0 | if (consistent) { |
1166 | 0 | if (server->d_config.useECS != useECS) { |
1167 | 0 | consistent = false; |
1168 | 0 | } |
1169 | 0 | if (server->d_config.disableZeroScope == zeroScope) { |
1170 | 0 | consistent = false; |
1171 | 0 | } |
1172 | 0 | } |
1173 | 0 | if (server->isTCPOnly() != tcpOnly) { |
1174 | 0 | consistent = false; |
1175 | 0 | tcpOnly = false; |
1176 | 0 | } |
1177 | 0 | } |
1178 | |
|
1179 | 0 | d_tcpOnly = tcpOnly; |
1180 | 0 | if (consistent) { |
1181 | | /* at this point we know that all servers agree |
1182 | | on these settings, so let's just use the same |
1183 | | values for the pool itself */ |
1184 | 0 | d_useECS = useECS; |
1185 | 0 | d_zeroScope = zeroScope; |
1186 | 0 | } |
1187 | 0 | d_isConsistent = consistent; |
1188 | 0 | } |
1189 | | |
1190 | | void ServerPool::setZeroScope(bool enabled) |
1191 | 0 | { |
1192 | 0 | d_zeroScope = enabled; |
1193 | 0 | updateConsistency(); |
1194 | 0 | } |
1195 | | |
1196 | | void ServerPool::setECS(bool useECS) |
1197 | 0 | { |
1198 | 0 | d_useECS = useECS; |
1199 | 0 | updateConsistency(); |
1200 | 0 | } |
1201 | | |
1202 | | bool ServerPool::shouldKeepStaleData() const |
1203 | 0 | { |
1204 | 0 | return !getServers().empty() && countServers(true) == 0; |
1205 | 0 | } |
1206 | | |
1207 | | namespace dnsdist::backend |
1208 | | { |
1209 | | void registerNewBackend(std::shared_ptr<DownstreamState>& backend) |
1210 | 0 | { |
1211 | 0 | dnsdist::configuration::updateRuntimeConfiguration([&backend](dnsdist::configuration::RuntimeConfiguration& config) { |
1212 | 0 | auto& backends = config.d_backends; |
1213 | 0 | backends.push_back(backend); |
1214 | 0 | std::stable_sort(backends.begin(), backends.end(), [](const std::shared_ptr<DownstreamState>& lhs, const std::shared_ptr<DownstreamState>& rhs) { |
1215 | 0 | return lhs->d_config.order < rhs->d_config.order; |
1216 | 0 | }); |
1217 | 0 | }); |
1218 | 0 | } |
1219 | | } |