2 The software in this package is distributed under the GNU General
3 Public License version 2 (with a special exception described below).
5 A copy of GNU General Public License (GPL) is included in this distribution,
6 in the file COPYING.GPL.
8 As a special exception, if other files instantiate templates or use macros
9 or inline functions from this file, or you compile this file and link it
10 with other works to produce a work based on this file, this file
11 does not by itself cause the resulting work to be covered
12 by the GNU General Public License.
14 However the source code for this file must still be made available
15 in accordance with section (3) of the GNU General Public License.
17 This exception does not invalidate any other reasons why a work based
18 on this file might be covered by the GNU General Public License.
20 Christian Herdtweck, Intra2net AG 2015
22 with code copied from boost::net::dns::resolve.hpp
23 by Andreas Haberstroh (andreas at ibusy dot com)
24 from https://github.com/softwareace/Boost.DNS
27 #include "dns/dnsresolver.h"
32 #include <boost/foreach.hpp>
33 #include <boost/bind.hpp>
34 #include <boost/function.hpp>
35 #include <boost/net/dns.hpp>
36 #include <boost/date_time/posix_time/posix_time.hpp>
37 #include <boost/uuid/uuid.hpp>
38 #include <boost/uuid/uuid_io.hpp>
40 #include <logfunc.hpp>
42 using I2n::Logger::GlobalLogger;
43 using boost::posix_time::seconds;
44 using boost::posix_time::minutes;
48 const int ResolveTimeoutSeconds = 0;
49 const int PauseBeforeRetrySeconds = 10;
50 const int StaleDataLongtermMinutes = 15;
51 const int DNS_PORT = 53;
54 DnsResolver::DnsResolver(IoServiceItem &io_serv,
55 const std::string &hostname,
56 const DnsIpProtocol &protocol,
57 const DnsCacheItem cache,
58 const boost::asio::ip::address &name_server)
59 : ResolverBase( io_serv, hostname, cache )
60 , Socket( *io_serv, ip::udp::endpoint(ip::udp::v4(), 0))
61 // just connect to anything, will specify sender/receiver later
64 , Protocol( protocol )
65 , NameServer( name_server, Config::DNS_PORT )
66 , ResolveTimeoutTimer( *io_serv )
67 , PauseBeforeRetryTimer( *io_serv )
68 , StaleDataLongtermTimer( *io_serv )
71 , IsResolving( false )
72 , LogPrefix( "DnsResolver" )
76 , OperationCancelled( false )
78 std::stringstream temp;
79 temp << "Dns(" << ResolverBase::Hostname << "): ";
80 LogPrefix = temp.str();
84 DnsResolver::~DnsResolver()
86 boost::system::error_code error;
87 //Socket.shutdown(boost::asio::ip::udp::socket::shutdown_both, error);
89 // GlobalLogger.warning() << LogPrefix << "Received error " << error
90 // << " when shutting down socket for DNS";
91 // in IcmpPinger always gave an error system:9 (EBADF: Bad file descriptor)
92 // Here gives error system:107 ENOTCONN: Transport endpoint is not connected
96 GlobalLogger.warning() << LogPrefix << "Received error " << error
97 << " when closing socket for DNS";
102 //==============================================================================
104 //==============================================================================
107 * copied here code from boost::net::dns::resolve.hpp, since want async
108 * operation and that is used only internally, there
110 void DnsResolver::do_resolve()
112 // check if resolving already
115 GlobalLogger.info() << LogPrefix
116 << "Call to do_resolve ignored since resolving already";
120 OperationCancelled = false;
122 GlobalLogger.info() << LogPrefix << "start resolving for IPs of type "
123 << to_string(Protocol) << " using name server " << NameServer;
125 // just to be sure: cancel timers
126 ResolveTimeoutTimer.cancel();
127 PauseBeforeRetryTimer.cancel();
128 StaleDataLongtermTimer.cancel();
130 // create DNS request
131 boost::net::dns::message dns_message( ResolverBase::Hostname, Protocol );
132 dns_message.recursive(false);
133 dns_message.action(boost::net::dns::message::query);
134 dns_message.opcode(boost::net::dns::message::squery);
136 // create random ID for message
137 boost::uuids::uuid message_id = RandomIdGenerator();
138 memcpy( &RequestId, message_id.data, sizeof(RequestId) );
139 dns_message.id( RequestId );
140 GlobalLogger.debug() << LogPrefix << "Request has ID "
141 << std::showbase << std::hex << dns_message.id();
143 // setup receipt of reply
144 Socket.async_receive_from(
145 boost::asio::buffer(ReceiveBuffer.get_array()),
147 boost::bind( &DnsResolver::handle_dns_result, this,
148 boost::asio::placeholders::error,
149 boost::asio::placeholders::bytes_transferred)
153 (void) ResolveTimeoutTimer.expires_from_now(
154 seconds(Config::ResolveTimeoutSeconds));
155 ResolveTimeoutTimer.async_wait( boost::bind(
156 &DnsResolver::handle_resolve_timeout,
157 this, boost::asio::placeholders::error) );
160 dns_message.encode(RequestBuffer);
164 bytes_sent = Socket.send_to(
165 boost::asio::buffer(RequestBuffer.get_array()),
168 catch (boost::system::system_error &err)
170 GlobalLogger.warning() << LogPrefix
171 << "Sending of DNS request message failed: "
177 if ( bytes_sent == 0 )
179 GlobalLogger.warning() << LogPrefix << "Empty DNS request sent!";
186 void DnsResolver::handle_dns_result(const boost::system::error_code &error,
187 const std::size_t bytes_transferred)
191 GlobalLogger.info() << LogPrefix << "DNS resolve resulted in error "
192 << error << " --> try again after a little while";
196 else if ( OperationCancelled )
197 { // async_resolve was cancelled --> callbacks already called
198 GlobalLogger.info() << LogPrefix
199 << "Ignoring DNS results since we were cancelled";
203 GlobalLogger.debug() << LogPrefix << "Handling DNS result ("
204 << bytes_transferred << " bytes transferred)";
206 // next 3(+1) lines copied from boost/net/dns/resolver.hpp:
207 // clamp the recvBuffer with the number of bytes transferred or decode buffr
208 ReceiveBuffer.length(bytes_transferred);
209 boost::net::dns::message result_message;
210 result_message.decode( ReceiveBuffer );
213 if (RequestId != result_message.id())
214 GlobalLogger.warning() << LogPrefix << "Received answer for request ID "
215 << std::showbase << std::hex << result_message.id()
216 << " but expected ID " << RequestId;
218 GlobalLogger.debug() << LogPrefix << "Result has correct ID "
219 << std::showbase << std::hex << RequestId;
222 // loop over answers, remembering ips and cnames
223 // work with a regular pointer to list of answers since result_message is
224 // owner of data and that exists until end of function
225 // Items in answers list are shared_ptr to resource_base_t
226 std::vector<host_addr_pair> result_ips;
227 std::vector<string_pair> result_cnames;
228 std::vector<string_pair> result_nameservers;
230 gather_results(result_message.answers(), &result_ips, &result_cnames,
231 &result_nameservers);
232 // results should have the logical order
233 // Hostname [ --> cname1 --> cname2 --> ... --> cnameN ] [ --> ips ]
235 // remember cname list (if there were any)
236 BOOST_FOREACH( const string_pair &host_and_cname, result_cnames )
237 ResolverBase::update_cache(host_and_cname.first, host_and_cname.second);
239 if ( !result_ips.empty() )
240 handle_ips( result_ips );
241 else if ( !result_cnames.empty() )
242 // no IPs but at least one cname --> find the "last" cname and
243 // re-start resolving with that
244 handle_cname(result_cnames);
246 { // no answers --> check for nameservers in authorities section
247 if ( !result_nameservers.empty() )
248 GlobalLogger.warning() << LogPrefix
249 << "Received NS records in answers! "
250 << "That is quite unexpected...";
251 gather_results(result_message.authorites(), &result_ips,
252 &result_cnames, &result_nameservers);
253 gather_results(result_message.additionals(), &result_ips,
254 &result_cnames, &result_nameservers);
256 // search for a nameserver for which an IP is given
257 bool have_recursed = false;
258 BOOST_FOREACH( const string_pair &nameserver, result_nameservers )
260 // go through ips and look for match
261 BOOST_FOREACH( const host_addr_pair &host_and_addr, result_ips )
263 if (nameserver.second == host_and_addr.first)
265 GlobalLogger.info() << LogPrefix << "Ask next nameserver "
266 << nameserver.second << " with IP "
267 << host_and_addr.second.get_ip() << " (responsible for "
268 << nameserver.first << ")";
269 have_recursed = true;
270 handle_recurse( host_and_addr.second );
278 if ( !have_recursed )
279 { // no nameserver with ip found -- strange
280 if (result_nameservers.empty())
282 GlobalLogger.error() << LogPrefix << "Result contained neither "
283 << "IP nor CNAME nor name server --> cannot proceed!";
284 handle_unavailable();
288 // TODO: check in cache for nameserver ips?
290 GlobalLogger.warning() << LogPrefix
291 << "There are " << result_nameservers.size()
292 << " nameservers given but none with IP "
293 << "--> need to resolve nameserver -- this sucks!";
294 //handle_recurse_without_ip(result_nameservers[0].second);
296 // would have to create a new resolver with previous nameserver
297 // to resolve new nameserver name; save in Recursor
298 // In callback reset Recursor, get ip(s) and continue in
300 GlobalLogger.warning() << LogPrefix << "Have not implemented "
301 << "resolution of name server; I sincerely hope this never "
302 << "happens or can be dealt with more easily another way!";
303 handle_unavailable();
309 void DnsResolver::gather_results(const boost::net::dns::rr_list_t *answers,
310 std::vector<host_addr_pair> *result_ips,
311 std::vector<string_pair> *result_cnames,
312 std::vector<string_pair> *result_nameservers)
315 using boost::net::dns::resource_base_t;
316 BOOST_FOREACH( boost::shared_ptr<resource_base_t> rr_item, *answers )
318 boost::net::dns::type_t rr_type = rr_item->rtype();
319 uint32_t ttl = rr_item->ttl();
320 std::string domain = rr_item->domain();
322 if (rr_type == boost::net::dns::type_a)
323 { // 'A' resource records carry IPv4 addresses
324 if (Protocol == DNS_IPv6)
326 GlobalLogger.info() << LogPrefix << "Ignoring IPv4 address "
327 << "because resolver was configured to only use IPv6.";
330 boost::asio::ip::address_v4 ip =
331 ( dynamic_cast<boost::net::dns::a_resource *> (rr_item.get()) )
333 result_ips->push_back(host_addr_pair(domain, HostAddress(ip, ttl)));
334 GlobalLogger.debug() << LogPrefix << "IPv4 " << ip << " with TTL "
335 << ttl << "s for " << domain;
337 else if (rr_type == boost::net::dns::type_a6)
338 { // 'AAAA' resource records carry IPv6 addresses
339 if (Protocol == DNS_IPv4)
341 GlobalLogger.info() << LogPrefix << "Ignoring IPv6 address "
342 << "because resolver was configured to only use IPv4.";
345 boost::asio::ip::address_v6 ip =
346 ( dynamic_cast<boost::net::dns::a6_resource *> (rr_item.get()) )
348 result_ips->push_back(host_addr_pair(domain, HostAddress(ip, ttl)));
349 GlobalLogger.debug() << LogPrefix << "IPv6 " << ip << " with TTL "
350 << ttl << "s for " << domain;
352 else if (rr_type == boost::net::dns::type_cname)
353 { // 'CNAME' resource records that carry aliases
355 (dynamic_cast<boost::net::dns::cname_resource *>(rr_item.get()))
357 result_cnames->push_back( string_pair(domain, cname) );
358 GlobalLogger.debug() << LogPrefix << "CNAME " << cname
359 << " with TTL " << ttl << "s for " << domain;
361 else if (rr_type == boost::net::dns::type_ns)
362 { // NS (nameserver) resource records
363 std::string nameserver =
364 (dynamic_cast<boost::net::dns::ns_resource *>(rr_item.get()))
366 result_nameservers->push_back( string_pair(domain, nameserver) );
367 GlobalLogger.debug() << LogPrefix << "NameServer " << nameserver
368 << " with TTL " << ttl << "s for " << domain;
370 else if (rr_type == boost::net::dns::type_soa)
371 GlobalLogger.debug() << LogPrefix << "SOA resource";
372 else if (rr_type == boost::net::dns::type_ptr)
373 GlobalLogger.debug() << LogPrefix << "ptr resource";
374 else if (rr_type == boost::net::dns::type_hinfo)
375 GlobalLogger.debug() << LogPrefix << "hinfo resource";
376 else if (rr_type == boost::net::dns::type_mx)
377 GlobalLogger.debug() << LogPrefix << "mx resource";
378 else if (rr_type == boost::net::dns::type_txt)
379 GlobalLogger.debug() << LogPrefix << "txt resource";
380 else if (rr_type == boost::net::dns::type_srv)
381 GlobalLogger.debug() << LogPrefix << "srv resource";
382 else if (rr_type == boost::net::dns::type_axfr)
383 GlobalLogger.debug() << LogPrefix << "axfr resource";
385 GlobalLogger.debug() << LogPrefix << "unknown resource type: "
386 << std::showbase << std::hex
387 << static_cast<unsigned>(rr_item->rtype());
392 void DnsResolver::handle_unavailable()
394 // schedule new attempt in quite a while
395 StaleDataLongtermTimer.expires_from_now(
396 minutes(Config::StaleDataLongtermMinutes));
397 StaleDataLongtermTimer.async_wait(
398 boost::bind( &DnsResolver::wait_timer_timeout_handler,
399 this, boost::asio::placeholders::error
403 // for now, admit failure
404 bool was_success = false;
405 finalize_resolve(was_success);
409 void DnsResolver::handle_ips(const std::vector<host_addr_pair> &result_ips)
411 // received at least one IP which could be for the queried host name
412 // or the cname at the "end" of the cname list;
413 // but all IPs should be for the same
414 HostAddressVec addr_list;
415 std::string only_host_for_ips = result_ips[0].first;
416 BOOST_FOREACH( const host_addr_pair &host_and_addr, result_ips)
418 if ( host_and_addr.first != only_host_for_ips )
419 GlobalLogger.warning() << LogPrefix
420 << "Received IPs for different hosts " << only_host_for_ips
421 << " and " << host_and_addr.first << " in one DNS result! "
422 << "--> ignore second";
425 GlobalLogger.notice() << LogPrefix << "Found IP "
426 << host_and_addr.second.get_ip() << " with TTL "
427 << host_and_addr.second.get_ttl().get_value() << "s";
428 addr_list.push_back(host_and_addr.second);
431 ResolverBase::update_cache( only_host_for_ips, addr_list );
434 bool was_success = true;
435 finalize_resolve(was_success);
439 void DnsResolver::handle_cname(const std::vector<string_pair> &result_cnames)
441 // find the "last" cname in the list
442 // Hostname --> cname1 --> cname2 --> ... --> cnameN
443 // We assume here that this list might not be in order but that all cnames
444 // form a single list (without a break)
445 std::string last_cname = "";
447 BOOST_REVERSE_FOREACH( const string_pair &host_and_cname, result_cnames )
449 could_be_last = true;
450 BOOST_REVERSE_FOREACH( const string_pair &other, result_cnames )
452 if (other.first == host_and_cname.second)
453 { // found cname for current cname
454 could_be_last = false;
460 last_cname = host_and_cname.second;
465 if (last_cname.empty())
467 GlobalLogger.error() << LogPrefix
468 << "Could not identify \"last\" CNAME to handle -- "
469 << "maybe we encountered a CNAME loop? Anyway, cannot proceed!";
470 GlobalLogger.info() << LogPrefix << "Result CNAMEs were:";
471 BOOST_FOREACH( const string_pair &host_and_cname, result_cnames )
472 GlobalLogger.info() << LogPrefix << host_and_cname.first << " --> "
473 << host_and_cname.second;
474 handle_unavailable();
477 { // check cache for IP for this cname
478 bool check_up_to_date = true;
479 HostAddressVec cached_data = Cache->get_ips_recursive(last_cname,
481 if ( !cached_data.empty() )
483 bool was_success = true;
484 int recursion_count = 1; // define cache access as only 1
485 finalize_resolve(was_success, recursion_count);
488 { // get resolver for canonical name
489 // as opposed to the interal Recursor variable used in
490 // handle_recurse, this is a "proper" resolver that is maintained
491 // and cached by DnsMaster --> independent of this Resolver
492 ResolverItem resolver = DnsMaster::get_instance()
493 ->get_resolver_for(last_cname, Protocol);
494 callback_type callback = boost::bind(
495 &DnsResolver::cname_resolve_callback,
497 resolver->async_resolve( callback );
499 // treat a CNAME as a partial result: not enough to run callbacks
500 // from finalize_resolve, but enough to stop timers and reset
501 // RetryCount --> name resolution can take longer
508 void DnsResolver::cname_resolve_callback(const bool was_success,
509 const int recursion_count)
511 if ( OperationCancelled )
512 { // async_resolve was cancelled --> callbacks already called
513 GlobalLogger.info() << LogPrefix
514 << "Ignoring CNAME results since we were cancelled";
517 else if (was_success)
518 GlobalLogger.debug() << LogPrefix << "CNAME resolution succeeded";
520 GlobalLogger.info() << LogPrefix << "CNAME resolution failed";
521 // no use to schedule retry in this case since cname resolver must have
522 // failed several times and we can just re-start the same procedure with
523 // the same information (in recursion can try different name server)
524 // --> no schedule_retry
526 // cname counts like one more recursion step ...
527 finalize_resolve(was_success, recursion_count+1);
531 void DnsResolver::handle_recurse(const HostAddress &name_server)
533 // get resolver for same hostname but using a different name server
536 if (Recursor->is_resolving())
538 GlobalLogger.warning() << LogPrefix << "Recursor is resolving! "
539 << "Will cancel and reset";
540 Recursor->cancel_resolve();
543 GlobalLogger.warning() << LogPrefix
544 << "Recursor has not been reset!";
548 Recursor = DnsMaster::get_instance()->get_recursor_for(
549 Hostname, Protocol, name_server.get_ip());
550 callback_type callback = boost::bind(
551 &DnsResolver::recursive_resolve_callback,
552 this, name_server.get_ttl().get_value(),
554 Recursor->async_resolve( callback );
556 // do not cancel timers or reset RetryCount
561 void DnsResolver::recursive_resolve_callback(const uint32_t min_ttl,
562 const bool was_success,
563 const int recursion_count)
566 << "Recursion back at request with name server " << NameServer;
568 // do not need recursor any more; next time re-create from different random
569 // name server; may have been reset already in cancel_resolve(), so that
570 // is ok. If not, issue a warning
573 if ( !OperationCancelled )
574 GlobalLogger.warning() << LogPrefix
575 << "Recursor was reset before callback!";
580 f ( OperationCancelled )
581 { // async_resolve was cancelled --> callbacks already called
582 GlobalLogger.info() << LogPrefix
583 << "Ignoring recursion results since we were cancelled";
586 else if (was_success)
588 // make sure the saved TTL is not larger than the one we found here
589 ResolverBase::update_cache_ttl(min_ttl);
590 finalize_resolve(was_success, recursion_count+1);
594 GlobalLogger.info() << LogPrefix << "Recursive resolution failed";
600 void DnsResolver::finalize_resolve(const bool was_success,
601 const int recursion_count)
603 // some consistency checks; failure might indicate a situation I had not
604 // anticipated during programming but might not be harmfull yet
606 GlobalLogger.warning() << LogPrefix << "Consistency check failed: "
607 << "not resolving any more!";
608 if ( OperationCancelled )
609 GlobalLogger.warning() << LogPrefix << "Consistency check failed: "
610 << " was cancelled!";
611 if ( ResolverBase::CallbackList.empty() )
612 GlobalLogger.warning() << LogPrefix << "Consistency check failed: "
614 if ( RequestId != 0 )
615 GlobalLogger.warning() << LogPrefix << "Consistency check failed: "
616 << "waiting for DNS reply!";
621 // schedule callbacks, clearing callback list
622 ResolverBase::schedule_callbacks(was_success, recursion_count);
625 GlobalLogger.notice() << LogPrefix << "finalized resolve"
626 << " with success = " << was_success
627 << " and recursion_count = " << recursion_count;
632 void DnsResolver::stop_trying()
635 GlobalLogger.debug() << LogPrefix << "Cancelling timers";
636 ResolveTimeoutTimer.cancel();
637 PauseBeforeRetryTimer.cancel();
638 StaleDataLongtermTimer.cancel();
645 bool DnsResolver::is_resolving()
651 void DnsResolver::cancel_resolve()
655 GlobalLogger.info() << LogPrefix
656 << "Cancel called on non-resolving resolver -- ignore";
659 else if (OperationCancelled)
661 GlobalLogger.info() << LogPrefix
662 << "Cancel called on cancelled resolver -- ignore";
667 Recursor->cancel_resolve(); // does not hurt even if it is not resolving
669 // set before finalize_resolve so can check in finalize_resolve that ID is
670 // always 0; ID is not used any more since handle_dns_result stops if
671 // OperationCancelled is true
674 bool was_success = false;
675 int recursion_count = 1;
676 finalize_resolve(was_success, recursion_count);
678 // set after finalize_resolve, so can check in finalize_resolve that
679 // cancel is never true
680 OperationCancelled = true;
685 void DnsResolver::handle_resolve_timeout(const boost::system::error_code &error)
687 if ( error == boost::asio::error::operation_aborted ) // cancelled
689 GlobalLogger.warning() << LogPrefix
690 << "Resolve timeout timer was cancelled!";
695 GlobalLogger.warning() << LogPrefix
696 << "resolve timeout handler received error "
700 else if ( OperationCancelled )
701 { // async_resolve was cancelled --> callbacks already called
702 GlobalLogger.info() << LogPrefix
703 << "Ignoring DNS timeout since we were cancelled";
708 GlobalLogger.notice() << LogPrefix << "DNS resolving timed out";
715 void DnsResolver::schedule_retry()
723 ResolveTimeoutTimer.cancel();
724 PauseBeforeRetryTimer.cancel();
729 if ( RetryCount > DnsMaster::get_instance()
730 ->get_max_address_resolution_attempts() )
731 { // too many re-tries
732 GlobalLogger.info() << LogPrefix << "Not scheduling a retry since "
733 << "RetryCount " << RetryCount << " too high";
734 handle_unavailable(); // will call stop_trying i.e. reset RetryCount
738 GlobalLogger.info() << LogPrefix << "Scheduling a retry (RetryCount="
739 << RetryCount << ")";
740 PauseBeforeRetryTimer.expires_from_now(
741 seconds(Config::PauseBeforeRetrySeconds));
742 PauseBeforeRetryTimer.async_wait(
743 boost::bind( &DnsResolver::wait_timer_timeout_handler,
744 this, boost::asio::placeholders::error) );
748 void DnsResolver::wait_timer_timeout_handler(
749 const boost::system::error_code &error)
751 if ( error == boost::asio::error::operation_aborted ) // cancelled
752 { // assume that our code cancelled this timer, so callbacks will be
754 GlobalLogger.warning() << LogPrefix
755 << "Resolve wait timer was cancelled! ";
758 { // not sure what to do here, but callers waiting forever for a callback
759 // is probably the worst thing to happen, so call finalize_resolve
760 GlobalLogger.warning() << LogPrefix
761 << "resolve wait handler received error "
762 << error << "! Try to finalize resolve";
763 bool was_success = false;
764 finalize_resolve(was_success);
766 else if ( OperationCancelled )
767 { // async_resolve was cancelled --> callbacks already called
768 GlobalLogger.info() << LogPrefix
769 << "Ignoring waiting timeout since we were cancelled";
774 GlobalLogger.info() << LogPrefix << "Done waiting --> re-try resolve";
780 //==============================================================================
782 //==============================================================================
784 HostAddress DnsResolver::get_next_ip()
787 HostAddressVec cached_data = ResolverBase::get_cached_ips_recursively();
789 // if no results cached, return default-constructed HostAddress (0.0.0.0)
790 if ( cached_data.empty() )
792 HostAddress return_value;
796 // check validity of index (cache may have changed since last call)
797 if (NextIpIndex >= cached_data.size())
801 return cached_data[NextIpIndex++];
804 bool DnsResolver::have_up_to_date_ip()
807 HostAddressVec cached_data = ResolverBase::get_cached_ips_recursively();
810 uint32_t resolved_ip_ttl_threshold = static_cast<uint32_t>(
811 DnsMaster::get_instance()->get_resolved_ip_ttl_threshold() );
813 // loop over addresses
814 BOOST_FOREACH( const HostAddress &addr, cached_data )
816 uint32_t ttl = addr.get_ttl().get_updated_value();
817 if ( ttl > resolved_ip_ttl_threshold )
821 // if not returned true by now, we have tried all IPs without success
825 int DnsResolver::get_resolved_ip_count()
827 return ResolverBase::get_cached_ips_recursively().size();
830 // (created using vim -- the world's best text editor)