2 * This file is part of PowerDNS or dnsdist.
3 * Copyright -- PowerDNS.COM B.V. and its contributors
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.
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.
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.
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.
33 #include <boost/program_options.hpp>
35 #include "dns_random.hh"
36 #include "dnsparser.hh"
37 #include "dnswriter.hh"
38 #include "dnsrecords.hh"
39 #include "ednsoptions.hh"
40 #include "ednssubnet.hh"
46 using std::unique_ptr
;
50 static std::atomic
<unsigned int> g_recvcounter
, g_recvbytes
;
51 static volatile bool g_done
;
53 namespace po
= boost::program_options
;
54 static po::variables_map g_vm
;
58 //NOLINTNEXTLINE(performance-unnecessary-value-param): we do want a copy to increase the reference count, thank you very much
59 static void recvThread(const std::shared_ptr
<std::vector
<std::unique_ptr
<Socket
>>> sockets
)
61 vector
<pollfd
> rfds
, fds
;
62 for (const auto& s
: *sockets
) {
67 pfd
.fd
= s
->getHandle();
76 vector
<struct mmsghdr
> buf(100);
78 cmsgbuf_aligned
*cbuf
= new cmsgbuf_aligned
;
79 fillMSGHdr(&m
.msg_hdr
, new struct iovec
, cbuf
, sizeof(*cbuf
), new char[1500], 1500, new ComboAddress("127.0.0.1"));
83 cmsgbuf_aligned
*cbuf
= new cmsgbuf_aligned
;
84 fillMSGHdr(&buf
, new struct iovec
, cbuf
, sizeof(*cbuf
), new char[1500], 1500, new ComboAddress("127.0.0.1"));
90 err
= poll(&fds
[0], fds
.size(), -1);
94 unixDie("Unable to poll for new UDP events");
97 for(auto &pfd
: fds
) {
98 if (pfd
.revents
& POLLIN
) {
100 if ((err
=recvmmsg(pfd
.fd
, &buf
[0], buf
.size(), MSG_WAITFORONE
, 0)) < 0 ) {
106 for(int n
=0; n
< err
; ++n
)
107 g_recvbytes
+= buf
[n
].msg_len
;
109 if ((err
= recvmsg(pfd
.fd
, &buf
, 0)) < 0) {
115 for (decltype(buf
.msg_iovlen
) i
= 0; i
< buf
.msg_iovlen
; i
++)
116 g_recvbytes
+= buf
.msg_iov
[i
].iov_len
;
123 static ComboAddress
getRandomAddressFromRange(const Netmask
& ecsRange
)
125 ComboAddress result
= ecsRange
.getMaskedNetwork();
126 uint8_t bits
= ecsRange
.getBits();
128 uint32_t mod
= 1 << (32 - bits
);
129 result
.sin4
.sin_addr
.s_addr
= result
.sin4
.sin_addr
.s_addr
+ htonl(dns_random(mod
));
132 result
.sin4
.sin_addr
.s_addr
= dns_random_uint32();
138 static void replaceEDNSClientSubnet(vector
<uint8_t>* packet
, const Netmask
& ecsRange
)
140 /* the last 4 bytes of the packet are the IPv4 address */
141 ComboAddress rnd
= getRandomAddressFromRange(ecsRange
);
142 uint32_t addr
= rnd
.sin4
.sin_addr
.s_addr
;
144 const auto packetSize
= packet
->size();
145 if (packetSize
< sizeof(addr
)) {
149 memcpy(&packet
->at(packetSize
- sizeof(addr
)), &addr
, sizeof(addr
));
152 static void sendPackets(const vector
<std::unique_ptr
<Socket
>>& sockets
, const vector
<vector
<uint8_t>* >& packets
, uint32_t qps
, ComboAddress dest
, const Netmask
& ecsRange
)
154 unsigned int burst
=100;
155 const auto nsecPerBurst
=1*(unsigned long)(burst
*1000000000.0/qps
);
156 struct timespec nsec
;
160 unsigned int nBursts
=0;
167 cmsgbuf_aligned cbuf
;
169 vector
<unique_ptr
<Unit
> > units
;
171 for(const auto& p
: packets
) {
176 if (!ecsRange
.empty()) {
177 replaceEDNSClientSubnet(p
, ecsRange
);
180 fillMSGHdr(&u
.msgh
, &u
.iov
, nullptr, 0, (char*)&(*p
)[0], p
->size(), &dest
);
182 auto socketHandle
= sockets
[count
% sockets
.size()]->getHandle();
183 ssize_t sendmsgRet
= sendmsg(socketHandle
, &u
.msgh
, 0);
184 if (sendmsgRet
!= 0) {
185 if (sendmsgRet
< 0) {
192 // Calculate the time in nsec we need to sleep to the next burst.
193 // If this is negative, it means that we are not achieving the requested
194 // target rate, in which case we skip the sleep.
195 int toSleep
= nBursts
*nsecPerBurst
- 1000*dt
.udiffNoReset();
197 nsec
.tv_nsec
= toSleep
;
204 static void usage(po::options_description
&desc
) {
205 cerr
<<"Syntax: calidns [OPTIONS] QUERY_FILE DESTINATION INITIAL_QPS HITRATE"<<endl
;
210 void parseQueryFile(const std::string
& queryFile
, vector
<std::shared_ptr
<vector
<uint8_t>>>& unknown
, bool useECSFromFile
, bool wantRecursion
, bool addECS
)
212 ifstream
ifs(queryFile
);
214 std::vector
<std::string
> fields
;
217 while (getline(ifs
, line
)) {
218 vector
<uint8_t> packet
;
219 DNSPacketWriter::optvect_t ednsOptions
;
221 if (line
.empty() || line
.at(0) == '#') {
226 stringtok(fields
, line
, "\t ");
227 if ((useECSFromFile
&& fields
.size() < 3) || fields
.size() < 2) {
228 cerr
<<"Skipping invalid line '"<<line
<<", it does not contain enough values"<<endl
;
232 const std::string
& qname
= fields
.at(0);
233 const std::string
& qtype
= fields
.at(1);
236 if (useECSFromFile
) {
237 subnet
= fields
.at(2);
240 DNSPacketWriter
packetWriter(packet
, DNSName(qname
), DNSRecordContent::TypeToNumber(qtype
));
241 packetWriter
.getHeader()->rd
= wantRecursion
;
242 packetWriter
.getHeader()->id
= dns_random_uint16();
244 if (!subnet
.empty() || addECS
) {
246 opt
.source
= Netmask(subnet
.empty() ? "0.0.0.0/32" : subnet
);
247 ednsOptions
.emplace_back(EDNSOptionCode::ECS
, makeEDNSSubnetOptsString(opt
));
250 if (!ednsOptions
.empty() || (packetWriter
.getHeader()->id
% 2) != 0) {
251 packetWriter
.addOpt(1500, 0, EDNSOpts::DNSSECOK
, ednsOptions
);
252 packetWriter
.commit();
254 unknown
.push_back(std::make_shared
<vector
<uint8_t>>(packet
));
257 shuffle(unknown
.begin(), unknown
.end(), pdns::dns_random_engine());
262 New plan. Set cache hit percentage, which we achieve on a per second basis.
263 So we start with 10000 qps for example, and for 90% cache hit ratio means
264 we take 1000 unique queries and each send them 10 times.
266 We then move the 1000 unique queries to the 'known' pool.
268 For the next second, say 20000 qps, we know we are going to need 2000 new queries,
269 so we take 2000 from the unknown pool. Then we need 18000 cache hits. We can get 1000 from
270 the known pool, leaving us down 17000. Or, we have 3000 in total now and we need 2000. We simply
271 repeat the 3000 mix we have ~7 times. The 2000 can now go to the known pool too.
273 For the next second, say 30000 qps, we'll need 3000 cache misses, which we get from
274 the unknown pool. To this we add 3000 queries from the known pool. Next up we repeat this batch 5
277 In general the algorithm therefore is:
279 1) Calculate number of cache misses required, get them from the unknown pool
280 2) Move those to the known pool
281 3) Fill up to amount of queries we need with random picks from the known pool
285 int main(int argc
, char** argv
)
288 po::options_description
desc("Options");
290 ("help,h", "Show this helpful message")
291 ("version", "Show the version number")
292 ("ecs", po::value
<string
>(), "Add EDNS Client Subnet option to outgoing queries using random addresses from the specified range (IPv4 only)")
293 ("ecs-from-file", "Read IP or subnet values from the query file and add them as EDNS Client Subnet options to outgoing queries")
294 ("increment", po::value
<float>()->default_value(1.1), "Set the factor to increase the QPS load per run")
295 ("maximum-qps", po::value
<uint32_t>(), "Stop incrementing once this rate has been reached, to provide a stable load")
296 ("minimum-success-rate", po::value
<double>()->default_value(0), "Stop the test as soon as the success rate drops below this value, in percent")
297 ("plot-file", po::value
<string
>(), "Write results to the specific file")
298 ("quiet", "Whether to run quietly, outputting only the maximum QPS reached. This option is mostly useful when used with --minimum-success-rate")
299 ("want-recursion", "Set the Recursion Desired flag on queries");
300 po::options_description alloptions
;
301 po::options_description
hidden("hidden options");
303 ("query-file", po::value
<string
>(), "File with queries")
304 ("destination", po::value
<string
>(), "Destination address")
305 ("initial-qps", po::value
<uint32_t>(), "Initial number of queries per second")
306 ("hitrate", po::value
<double>(), "Aim this percent cache hitrate");
308 alloptions
.add(desc
).add(hidden
);
309 po::positional_options_description p
;
310 p
.add("query-file", 1);
311 p
.add("destination", 1);
312 p
.add("initial-qps", 1);
315 po::store(po::command_line_parser(argc
, argv
).options(alloptions
).positional(p
).run(), g_vm
);
318 if (g_vm
.count("help")) {
323 if (g_vm
.count("version")) {
324 cerr
<<"calidns "<<VERSION
<<endl
;
328 if (!(g_vm
.count("query-file") && g_vm
.count("destination") && g_vm
.count("initial-qps") && g_vm
.count("hitrate"))) {
333 float increment
= 1.1;
335 increment
= g_vm
["increment"].as
<float>();
340 bool wantRecursion
= g_vm
.count("want-recursion");
341 bool useECSFromFile
= g_vm
.count("ecs-from-file");
342 g_quiet
= g_vm
.count("quiet");
344 double hitrate
= g_vm
["hitrate"].as
<double>();
345 if (hitrate
> 100 || hitrate
< 0) {
346 cerr
<<"hitrate must be between 0 and 100, not "<<hitrate
<<endl
;
350 uint32_t qpsstart
= g_vm
["initial-qps"].as
<uint32_t>();
352 uint32_t maximumQps
= std::numeric_limits
<uint32_t>::max();
353 if (g_vm
.count("maximum-qps")) {
354 maximumQps
= g_vm
["maximum-qps"].as
<uint32_t>();
357 double minimumSuccessRate
= g_vm
["minimum-success-rate"].as
<double>();
358 if (minimumSuccessRate
> 100.0 || minimumSuccessRate
< 0.0) {
359 cerr
<<"Minimum success rate must be between 0 and 100, not "<<minimumSuccessRate
<<endl
;
364 if (g_vm
.count("ecs")) {
367 ecsRange
= Netmask(g_vm
["ecs"].as
<string
>());
368 if (!ecsRange
.empty()) {
370 if (!ecsRange
.isIPv4()) {
371 cerr
<<"Only IPv4 ranges are supported for ECS at the moment!"<<endl
;
376 cout
<<"Adding ECS option to outgoing queries with random addresses from the "<<ecsRange
.toString()<<" range"<<endl
;
380 catch (const NetmaskException
& e
) {
381 cerr
<<"Error while parsing the ECS netmask: "<<e
.reason
<<endl
;
386 struct sched_param param
;
387 param
.sched_priority
=99;
389 #ifdef HAVE_SCHED_SETSCHEDULER
390 if(sched_setscheduler(0, SCHED_FIFO
, ¶m
) < 0) {
392 cerr
<<"Unable to set SCHED_FIFO: "<<stringerror()<<endl
;
398 vector
<std::shared_ptr
<vector
<uint8_t>>> unknown
;
399 vector
<std::shared_ptr
<vector
<uint8_t>>> known
;
400 parseQueryFile(g_vm
["query-file"].as
<string
>(), unknown
, useECSFromFile
, wantRecursion
, !ecsRange
.empty());
403 cout
<<"Generated "<<unknown
.size()<<" ready to use queries"<<endl
;
406 auto sockets
= std::make_shared
<std::vector
<std::unique_ptr
<Socket
>>>();
409 dest
= ComboAddress(g_vm
["destination"].as
<string
>(), 53);
411 catch (PDNSException
&e
) {
412 cerr
<<e
.reason
<<endl
;
415 for(int i
=0; i
< 24; ++i
) {
416 auto sock
= make_unique
<Socket
>(dest
.sin4
.sin_family
, SOCK_DGRAM
);
417 // sock->connect(dest);
419 setSocketSendBuffer(sock
->getHandle(), 2000000);
421 catch (const std::exception
& e
) {
423 cerr
<<e
.what()<<endl
;
427 setSocketReceiveBuffer(sock
->getHandle(), 2000000);
429 catch (const std::exception
& e
) {
431 cerr
<<e
.what()<<endl
;
435 sockets
->push_back(std::move(sock
));
439 std::thread
receiver(recvThread
, sockets
);
446 if (g_vm
.count("plot-file")) {
447 plot
.open(g_vm
["plot-file"].as
<string
>());
449 cerr
<<"Error opening "<<g_vm
["plot-file"].as
<string
>()<<" for writing: "<<stringerror()<<endl
;
454 double bestQPS
= 0.0;
455 double bestPerfectQPS
= 0.0;
457 for(qps
=qpsstart
;;) {
460 cout
<<"Aiming at "<<qps
<< "qps (RD="<<wantRecursion
<<") for "<<seconds
<<" seconds at cache hitrate "<<100.0*hitrate
<<"%";
462 unsigned int misses
=(1-hitrate
)*qps
*seconds
;
463 unsigned int total
=qps
*seconds
;
468 cout
<<", need "<<misses
<<" misses, "<<total
<<" queries, have "<<unknown
.size()<<" unknown left!"<<endl
;
471 if (misses
> unknown
.size()) {
472 cerr
<<"Not enough queries remaining (need at least "<<misses
<<" and got "<<unknown
.size()<<", please add more to the query file), exiting."<<endl
;
475 vector
<vector
<uint8_t>*> toSend
;
477 for(n
=0; n
< misses
; ++n
) {
478 auto ptr
=unknown
.back();
480 toSend
.push_back(ptr
.get());
481 known
.push_back(ptr
);
483 for(;n
< total
; ++n
) {
484 toSend
.push_back(known
[dns_random(known
.size())].get());
487 shuffle(toSend
.begin(), toSend
.end(), pdns::dns_random_engine());
488 g_recvcounter
.store(0);
493 sendPackets(*sockets
, toSend
, qps
, dest
, ecsRange
);
495 const auto udiff
= dt
.udiffNoReset();
496 const auto realqps
=toSend
.size()/(udiff
/1000000.0);
498 cout
<<"Achieved "<<realqps
<<" qps over "<< udiff
/1000000.0<<" seconds"<<endl
;
502 const auto received
= g_recvcounter
.load();
503 const auto udiffReceived
= dt
.udiff();
504 const auto realReceivedQPS
= received
/(udiffReceived
/1000000.0);
505 double perc
=received
*100.0/toSend
.size();
507 cout
<<"Received "<<received
<<" packets over "<< udiffReceived
/1000000.0<<" seconds ("<<perc
<<"%, adjusted received rate "<<realReceivedQPS
<<" qps)"<<endl
;
511 plot
<<qps
<<" "<<realqps
<<" "<<perc
<<" "<<received
/(udiff
/1000000.0)<<" " << 8*g_recvbytes
.load()/(udiff
/1000000.0)<<endl
;
515 if (qps
< maximumQps
) {
522 if (minimumSuccessRate
> 0.0 && perc
< minimumSuccessRate
) {
527 cout
<<"The latest success rate ("<<perc
<<") dropped below the minimum success rate of "<<minimumSuccessRate
<<", stopping."<<endl
;
528 cout
<<"The final rate reached before failing was "<<bestQPS
<<" qps (best rate at 100% was "<<bestPerfectQPS
<<" qps)"<<endl
;
533 bestQPS
= std::max(bestQPS
, realReceivedQPS
);
535 bestPerfectQPS
= std::max(bestPerfectQPS
, realReceivedQPS
);
545 catch (const std::exception
& exp
)
547 cerr
<<"Fatal error: "<<exp
.what()<<endl
;
550 catch (const NetmaskException
& exp
)
552 cerr
<<"Fatal error: "<<exp
.reason
<<endl
;