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 = 3;
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" )
75 , OperationCancelled( false )
77 std::stringstream temp;
78 temp << "Dns(" << ResolverBase::Hostname << "): ";
79 LogPrefix = temp.str();
83 DnsResolver::~DnsResolver()
85 boost::system::error_code error;
86 //Socket.shutdown(boost::asio::ip::udp::socket::shutdown_both, error);
88 // GlobalLogger.warning() << LogPrefix << "Received error " << error
89 // << " when shutting down socket for DNS";
90 // in IcmpPinger always gave an error system:9 (EBADF: Bad file descriptor)
91 // Here gives error system:107 ENOTCONN: Transport endpoint is not connected
95 GlobalLogger.warning() << LogPrefix << "Received error " << error
96 << " when closing socket for DNS";
101 //==============================================================================
103 //==============================================================================
106 * copied here code from boost::net::dns::resolve.hpp, since want async
107 * operation and that is used only internally, there
109 void DnsResolver::do_resolve()
111 // check if resolving already
114 GlobalLogger.info() << LogPrefix
115 << "Call to do_resolve ignored since resolving already";
119 OperationCancelled = false;
121 GlobalLogger.info() << LogPrefix << "start resolving for IPs of type "
122 << to_string(Protocol) << " using name server " << NameServer;
124 // just to be sure: cancel timers
125 ResolveTimeoutTimer.cancel();
126 PauseBeforeRetryTimer.cancel();
127 StaleDataLongtermTimer.cancel();
129 // create DNS request
130 boost::net::dns::message dns_message( ResolverBase::Hostname, Protocol );
131 dns_message.recursive(true);
132 dns_message.action(boost::net::dns::message::query);
133 dns_message.opcode(boost::net::dns::message::squery);
135 // create random ID for message
136 boost::uuids::uuid message_id = RandomIdGenerator();
137 memcpy( &RequestId, message_id.data, sizeof(RequestId) );
138 dns_message.id( RequestId );
139 GlobalLogger.debug() << LogPrefix << "Request has ID "
140 << std::showbase << std::hex << dns_message.id();
142 // setup receipt of reply
143 Socket.async_receive_from(
144 boost::asio::buffer(ReceiveBuffer.get_array()),
146 boost::bind( &DnsResolver::handle_dns_result, this,
147 boost::asio::placeholders::error,
148 boost::asio::placeholders::bytes_transferred)
152 (void) ResolveTimeoutTimer.expires_from_now(
153 seconds(Config::ResolveTimeoutSeconds));
154 ResolveTimeoutTimer.async_wait( boost::bind(
155 &DnsResolver::handle_resolve_timeout,
156 this, boost::asio::placeholders::error) );
159 dns_message.encode(RequestBuffer);
163 bytes_sent = Socket.send_to(
164 boost::asio::buffer(RequestBuffer.get_array()),
167 catch (boost::system::system_error &err)
169 GlobalLogger.warning() << LogPrefix
170 << "Sending of DNS request message failed: "
176 if ( bytes_sent == 0 )
178 GlobalLogger.warning() << LogPrefix << "Empty DNS request sent!";
185 void DnsResolver::handle_dns_result(const boost::system::error_code &error,
186 const std::size_t bytes_transferred)
190 GlobalLogger.info() << LogPrefix << "DNS resolve resulted in error "
191 << error << " --> try again after a little while";
195 else if ( OperationCancelled )
196 { // async_resolve was cancelled --> callbacks already called
197 GlobalLogger.info() << LogPrefix
198 << "Ignoring DNS results since we were cancelled";
202 GlobalLogger.debug() << LogPrefix << "Handling DNS result ("
203 << bytes_transferred << " bytes transferred)";
205 // next 3(+1) lines copied from boost/net/dns/resolver.hpp:
206 // clamp the recvBuffer with the number of bytes transferred or decode buffr
207 ReceiveBuffer.length(bytes_transferred);
208 boost::net::dns::message result_message;
209 result_message.decode( ReceiveBuffer );
212 if (RequestId != result_message.id())
213 GlobalLogger.warning() << LogPrefix << "Received answer for request ID "
214 << std::showbase << std::hex << result_message.id()
215 << " but expected ID " << RequestId;
217 GlobalLogger.debug() << LogPrefix << "Result has correct ID "
218 << std::showbase << std::hex << RequestId;
221 // loop over answers, remembering ips and cnames
222 // work with a regular pointer to list of answers since result_message is
223 // owner of data and that exists until end of function
224 // Items in answers list are shared_ptr to resource_base_t
225 std::vector<host_addr_pair> result_ips;
226 std::vector<src_cname_pair> result_cnames;
227 std::vector<string_pair> result_name_servers;
229 GlobalLogger.debug() << "Checking ANSWERS section of dns reply";
230 gather_results(result_message.answers(), &result_ips, &result_cnames,
231 &result_name_servers);
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 src_cname_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 --> cannot proceed
247 GlobalLogger.warning() << LogPrefix << "No IP nor CNAME received! "
248 << "--> schedule retry";
254 * gather IPs, CNAMEs and name servers from list of resource records;
256 * can be run on anwers(), autorities() and additional() sections of dns reply
259 * @param rr_list: input list of resource records
260 * @param result_ips: output vector of ips
261 * @param result_cnames: output vector of cnames
262 * @param result_name_servers: output vector of name servers
264 void DnsResolver::gather_results(const boost::net::dns::rr_list_t *rr_list,
265 std::vector<host_addr_pair> *result_ips,
266 std::vector<src_cname_pair> *result_cnames,
267 std::vector<string_pair> *result_name_servers)
270 using boost::net::dns::resource_base_t;
271 boost::posix_time::ptime now =boost::posix_time::second_clock::local_time();
272 BOOST_FOREACH( boost::shared_ptr<resource_base_t> rr_item, *rr_list )
274 boost::net::dns::type_t rr_type = rr_item->rtype();
275 uint32_t ttl = rr_item->ttl();
276 std::string domain = rr_item->domain();
278 boost::posix_time::to_simple_string(now + seconds(ttl));
280 if (rr_type == boost::net::dns::type_a)
281 { // 'A' resource records carry IPv4 addresses
282 if (Protocol == DNS_IPv6)
284 GlobalLogger.info() << LogPrefix << "Ignoring IPv4 address "
285 << "because resolver was configured to only use IPv6.";
288 boost::asio::ip::address_v4 ip =
289 ( dynamic_cast<boost::net::dns::a_resource *> (rr_item.get()) )
291 result_ips->push_back(host_addr_pair(domain, HostAddress(ip, ttl)));
292 GlobalLogger.debug() << LogPrefix << domain << ": IPv4 " << ip
293 << " with TTL " << ttl << "s (until "
296 else if (rr_type == boost::net::dns::type_a6)
297 { // 'AAAA' resource records carry IPv6 addresses
298 if (Protocol == DNS_IPv4)
300 GlobalLogger.info() << LogPrefix << "Ignoring IPv6 address "
301 << "because resolver was configured to only use IPv4.";
304 boost::asio::ip::address_v6 ip =
305 ( dynamic_cast<boost::net::dns::a6_resource *> (rr_item.get()) )
307 result_ips->push_back(host_addr_pair(domain, HostAddress(ip, ttl)));
308 GlobalLogger.debug() << LogPrefix << domain << ": IPv6 " << ip
309 << " with TTL " << ttl << "s (until "
312 else if (rr_type == boost::net::dns::type_cname)
313 { // 'CNAME' resource records that carry aliases
315 (dynamic_cast<boost::net::dns::cname_resource *>(rr_item.get()))
317 result_cnames->push_back( src_cname_pair(domain,
318 Cname(cname, ttl)) );
319 GlobalLogger.debug() << LogPrefix << domain << ": CNAME to "
320 << cname << " with TTL " << ttl << "s (until "
323 else if (rr_type == boost::net::dns::type_ns)
324 { // NS (name_server) resource records
325 std::string name_server =
326 (dynamic_cast<boost::net::dns::ns_resource *>(rr_item.get()))
328 result_name_servers->push_back( string_pair(domain, name_server) );
329 GlobalLogger.debug() << LogPrefix << "NameServer " << name_server
330 << " for " << domain << " with TTL " << ttl
331 << "s (until " << expiry << ")";
333 else if (rr_type == boost::net::dns::type_soa)
334 GlobalLogger.debug() << LogPrefix << "SOA resource";
335 else if (rr_type == boost::net::dns::type_ptr)
336 GlobalLogger.debug() << LogPrefix << "ptr resource";
337 else if (rr_type == boost::net::dns::type_hinfo)
338 GlobalLogger.debug() << LogPrefix << "hinfo resource";
339 else if (rr_type == boost::net::dns::type_mx)
340 GlobalLogger.debug() << LogPrefix << "mx resource";
341 else if (rr_type == boost::net::dns::type_txt)
342 GlobalLogger.debug() << LogPrefix << "txt resource";
343 else if (rr_type == boost::net::dns::type_srv)
344 GlobalLogger.debug() << LogPrefix << "srv resource";
345 else if (rr_type == boost::net::dns::type_axfr)
346 GlobalLogger.debug() << LogPrefix << "axfr resource";
348 GlobalLogger.debug() << LogPrefix << "unknown resource type: "
349 << std::showbase << std::hex
350 << static_cast<unsigned>(rr_item->rtype());
355 void DnsResolver::handle_unavailable()
357 // schedule new attempt in quite a while
358 StaleDataLongtermTimer.expires_from_now(
359 minutes(Config::StaleDataLongtermMinutes));
360 StaleDataLongtermTimer.async_wait(
361 boost::bind( &DnsResolver::wait_timer_timeout_handler,
362 this, boost::asio::placeholders::error
366 // for now, admit failure
367 bool was_success = false;
368 finalize_resolve(was_success);
372 void DnsResolver::handle_ips(const std::vector<host_addr_pair> &result_ips)
374 // received at least one IP which could be for the queried host name
375 // or the cname at the "end" of the cname list;
376 // but all IPs should be for the same
377 HostAddressVec addr_list;
378 std::string only_host_for_ips = result_ips[0].first;
379 BOOST_FOREACH( const host_addr_pair &host_and_addr, result_ips)
381 if ( host_and_addr.first != only_host_for_ips )
382 GlobalLogger.warning() << LogPrefix
383 << "Received IPs for different hosts " << only_host_for_ips
384 << " and " << host_and_addr.first << " in one DNS result! "
385 << "--> ignore second";
388 GlobalLogger.notice() << LogPrefix << "Found IP "
389 << host_and_addr.second.get_ip() << " with TTL "
390 << host_and_addr.second.get_ttl().get_value() << "s";
391 addr_list.push_back(host_and_addr.second);
394 ResolverBase::update_cache( only_host_for_ips, addr_list );
397 bool was_success = true;
398 finalize_resolve(was_success);
402 void DnsResolver::handle_cname(const std::vector<src_cname_pair> &result_cnames)
404 // find the "last" cname in the list
405 // Hostname --> cname1 --> cname2 --> ... --> cnameN
406 // We assume here that this list might not be in order but that all cnames
407 // form a single list (form one connected list and not several isolated)
409 std::string last_cname = "";
411 BOOST_REVERSE_FOREACH( const src_cname_pair &host_and_cname, result_cnames )
413 could_be_last = true;
414 BOOST_REVERSE_FOREACH( const src_cname_pair &other, result_cnames )
416 if (other.first == host_and_cname.second.Host)
417 { // found cname for current cname
418 could_be_last = false;
424 last_cname = host_and_cname.second.Host;
429 if (last_cname.empty())
431 GlobalLogger.error() << LogPrefix
432 << "Could not identify \"last\" CNAME to handle -- "
433 << "maybe we encountered a CNAME loop? Anyway, cannot proceed!";
434 GlobalLogger.info() << LogPrefix << "Result CNAMEs were:";
435 BOOST_FOREACH( const src_cname_pair &host_and_cname, result_cnames )
436 GlobalLogger.info() << LogPrefix << host_and_cname.first << " --> "
437 << host_and_cname.second.Host;
438 handle_unavailable();
441 { // check cache for IP for this cname
442 bool check_up_to_date = true;
443 HostAddressVec cached_data = Cache->get_ips_recursive(last_cname,
445 if ( !cached_data.empty() )
447 bool was_success = true;
448 int cname_count = 1; // define cache access as only 1
449 finalize_resolve(was_success, cname_count);
452 { // get resolver for canonical name
453 ResolverItem resolver = DnsMaster::get_instance()
454 ->get_resolver_for(last_cname, Protocol);
455 callback_type callback = boost::bind(
456 &DnsResolver::cname_resolve_callback,
458 resolver->async_resolve( callback );
460 // treat a CNAME as a partial result: not enough to run callbacks
461 // from finalize_resolve, but enough to stop timers and reset
462 // RetryCount --> name resolution can take longer
469 void DnsResolver::cname_resolve_callback(const bool was_success,
470 const int cname_count)
472 if ( OperationCancelled )
473 { // async_resolve was cancelled --> callbacks already called
474 GlobalLogger.info() << LogPrefix
475 << "Ignoring CNAME results since we were cancelled";
478 else if (was_success)
479 GlobalLogger.debug() << LogPrefix << "CNAME resolution succeeded";
481 GlobalLogger.info() << LogPrefix << "CNAME resolution failed";
482 // no use to schedule retry in this case since cname resolver must have
483 // failed several times and we can only re-start the same procedure with
484 // the same information
486 // cname counts like one more recursion step ...
487 finalize_resolve(was_success, cname_count+1);
491 void DnsResolver::finalize_resolve(const bool was_success,
492 const int cname_count)
494 // some consistency checks; failure might indicate a situation I had not
495 // anticipated during programming but might not be harmfull yet
497 GlobalLogger.warning() << LogPrefix << "Consistency check failed: "
498 << "not resolving any more!";
499 if ( OperationCancelled )
500 GlobalLogger.warning() << LogPrefix << "Consistency check failed: "
501 << " was cancelled!";
502 if ( ResolverBase::CallbackList.empty() )
503 GlobalLogger.warning() << LogPrefix << "Consistency check failed: "
505 if ( RequestId != 0 )
506 GlobalLogger.warning() << LogPrefix << "Consistency check failed: "
507 << "waiting for DNS reply!";
512 // schedule callbacks, clearing callback list
513 ResolverBase::schedule_callbacks(was_success, cname_count);
516 GlobalLogger.notice() << LogPrefix << "finalized resolve"
517 << " with success = " << was_success
518 << " and cname_count = " << cname_count;
523 void DnsResolver::stop_trying()
526 GlobalLogger.debug() << LogPrefix << "Cancelling timers";
527 ResolveTimeoutTimer.cancel();
528 PauseBeforeRetryTimer.cancel();
529 StaleDataLongtermTimer.cancel();
536 bool DnsResolver::is_resolving()
543 * cancel a earlier call to async_resolve
545 * callbacks will be called with was_success=false; all internal operations
546 * will be cancelled and internal callbacks (timers, dns results) have no
549 void DnsResolver::cancel_resolve()
553 GlobalLogger.info() << LogPrefix
554 << "Cancel called on non-resolving resolver -- ignore";
557 else if (OperationCancelled)
559 GlobalLogger.info() << LogPrefix
560 << "Cancel called on cancelled resolver -- ignore";
564 // set before finalize_resolve so can check in finalize_resolve that ID is
565 // always 0; ID is not used any more since handle_dns_result stops if
566 // OperationCancelled is true
569 bool was_success = false;
571 finalize_resolve(was_success, cname_count);
573 // set after finalize_resolve, so can check in finalize_resolve that
574 // OperationCancelled is never true
575 OperationCancelled = true;
580 void DnsResolver::handle_resolve_timeout(const boost::system::error_code &error)
582 if ( error == boost::asio::error::operation_aborted ) // cancelled
584 GlobalLogger.warning() << LogPrefix
585 << "Resolve timeout timer was cancelled!";
590 GlobalLogger.warning() << LogPrefix
591 << "resolve timeout handler received error "
592 << error << " --> retry";
595 else if ( OperationCancelled )
596 { // async_resolve was cancelled --> callbacks already called
597 GlobalLogger.info() << LogPrefix
598 << "Ignoring DNS timeout since we were cancelled";
603 GlobalLogger.notice() << LogPrefix << "DNS resolving timed out";
609 void DnsResolver::schedule_retry()
612 ResolveTimeoutTimer.cancel();
613 PauseBeforeRetryTimer.cancel();
618 if ( RetryCount > DnsMaster::get_instance()
619 ->get_max_address_resolution_attempts() )
620 { // too many re-tries
621 GlobalLogger.info() << LogPrefix << "Not scheduling a retry since "
622 << "RetryCount " << RetryCount << " too high";
623 handle_unavailable(); // will call stop_trying i.e. reset RetryCount
627 GlobalLogger.info() << LogPrefix << "Scheduling a retry (RetryCount="
628 << RetryCount << ")";
629 PauseBeforeRetryTimer.expires_from_now(
630 seconds(Config::PauseBeforeRetrySeconds));
631 PauseBeforeRetryTimer.async_wait(
632 boost::bind( &DnsResolver::wait_timer_timeout_handler,
633 this, boost::asio::placeholders::error) );
637 void DnsResolver::wait_timer_timeout_handler(
638 const boost::system::error_code &error)
640 if ( error == boost::asio::error::operation_aborted ) // cancelled
641 { // assume that our code cancelled this timer, so callbacks will be
643 GlobalLogger.warning() << LogPrefix
644 << "Resolve wait timer was cancelled! ";
647 { // not sure what to do here, but callers waiting forever for a callback
648 // is probably the worst thing to happen, so call finalize_resolve
649 GlobalLogger.warning() << LogPrefix
650 << "resolve wait handler received error "
651 << error << "! Try to finalize resolve";
652 bool was_success = false;
653 finalize_resolve(was_success);
655 else if ( OperationCancelled )
656 { // async_resolve was cancelled --> callbacks already called
657 GlobalLogger.info() << LogPrefix
658 << "Ignoring waiting timeout since we were cancelled";
663 GlobalLogger.info() << LogPrefix << "Done waiting --> re-try resolve";
669 //==============================================================================
671 //==============================================================================
673 HostAddress DnsResolver::get_next_ip(bool check_up_to_date)
676 // (do not use arg check_up_to_date here in order to give NextIpIndex
677 // a chance to stay above number of outdate IPs)
678 HostAddressVec cached_data = ResolverBase::get_cached_ips_recursively();
680 // if no results cached, return default-constructed HostAddress (0.0.0.0)
681 HostAddress return_candidate;
682 if ( cached_data.empty() )
684 GlobalLogger.debug() << LogPrefix << "Get next IP: nothing cached";
685 return return_candidate;
689 std::size_t n_ips = cached_data.size();
690 uint32_t ttl_thresh = static_cast<uint32_t>( DnsMaster::get_instance()
691 ->get_resolved_ip_ttl_threshold() );
693 GlobalLogger.info() << LogPrefix << "Get next IP from cached result of "
694 << n_ips << " IPs; first index to consider is " << NextIpIndex
695 << "; TTL thresh = " << ttl_thresh << " is used: " << check_up_to_date;
697 // loop until we have found a cached result (that is up to date)
698 // or until we have tried all cached IPs
701 // check index since cache size may have changed since last call
702 if (NextIpIndex >= n_ips)
704 else if ( n_iter >= n_ips)
705 return HostAddress(); // have checked all candidates
707 { // there are candidates left to consider
708 return_candidate = cached_data[NextIpIndex++];
709 if (!check_up_to_date)
710 return return_candidate;
711 else if (cached_data[NextIpIndex].get_ttl().get_updated_value()
713 return cached_data[++NextIpIndex];
720 bool DnsResolver::have_up_to_date_ip()
722 return get_resolved_ip_count() > 0;
725 int DnsResolver::get_resolved_ip_count()
727 return ResolverBase::get_cached_ips_recursively("", true).size();
730 // (created using vim -- the world's best text editor)