re-raise a little output in cache: state newly acquired IPs and CNAMEs
[pingcheck] / src / dns / dnscache.cpp
CommitLineData
96779587
CH
1/*
2 The software in this package is distributed under the GNU General
3 Public License version 2 (with a special exception described below).
4
5 A copy of GNU General Public License (GPL) is included in this distribution,
6 in the file COPYING.GPL.
7
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.
13
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.
16
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.
19
20 Christian Herdtweck, Intra2net AG 2015
21 */
22
c5b4902d 23#include "dns/dnscache.h"
96779587 24
f636ba88 25#include <sstream>
946356e1 26#include <fstream>
96779587 27#include <logfunc.hpp>
946356e1 28#include <filefunc.hxx> // I2n::file_exists
e18c1337 29#include <boost/foreach.hpp>
96779587
CH
30#include <boost/bind.hpp>
31#include <boost/date_time/posix_time/posix_time.hpp>
32#include <boost/asio/placeholders.hpp>
946356e1
CH
33#include <boost/serialization/serialization.hpp>
34#include <boost/serialization/map.hpp>
35#include <boost/serialization/string.hpp>
36#include <boost/serialization/vector.hpp>
37#include <boost/archive/xml_oarchive.hpp>
38#include <boost/archive/xml_iarchive.hpp>
96779587 39
dbe986b9
CH
40#include "dns/dnsmaster.h"
41
96779587
CH
42using boost::bind;
43using boost::posix_time::seconds;
44using I2n::Logger::GlobalLogger;
45
8d26221d 46
96779587
CH
47namespace Config
48{
49 int SaveTimerSeconds = 60;
50}
51
f833126b
CH
52// -----------------------------------------------------------------------------
53// Cname
54// -----------------------------------------------------------------------------
55
23f51766
CH
56Cname::Cname()
57 : Host()
58 , Ttl()
59{}
60
61Cname::Cname(const std::string &host, const uint32_t ttl)
62 : Host( host )
63 , Ttl( ttl )
64{}
65
26b0f687
CH
66Cname::Cname(const std::string &host, const TimeToLive &ttl)
67 : Host( host )
68 , Ttl( ttl )
69{}
70
8d26221d 71
f833126b
CH
72// -----------------------------------------------------------------------------
73// DNS Cache constructor / destructor
74// -----------------------------------------------------------------------------
75
8d26221d
CH
76const string DnsCache::DoNotUseCacheFile = "do not use cache file!";
77
96779587 78DnsCache::DnsCache(const IoServiceItem &io_serv,
f833126b
CH
79 const std::string &cache_file,
80 const uint32_t min_time_between_resolves)
ad83004d
CH
81 : IpCache()
82 , CnameCache()
96779587
CH
83 , SaveTimer( *io_serv )
84 , CacheFile( cache_file )
85 , HasChanged( false )
f833126b 86 , MinTimeBetweenResolves( min_time_between_resolves )
96779587
CH
87{
88 // load cache from file
89 load_from_cachefile();
90
91 // schedule next save
92 (void) SaveTimer.expires_from_now( seconds( Config::SaveTimerSeconds ) );
93 SaveTimer.async_wait( bind( &DnsCache::schedule_save, this,
94 boost::asio::placeholders::error ) );
95}
96
97
98DnsCache::~DnsCache()
99{
26b0f687 100 GlobalLogger.info() << "DnsCache: being destructed";
e91538f0 101
96779587
CH
102 // save one last time without re-scheduling the next save
103 save_to_cachefile();
104
105 // cancel save timer
106 SaveTimer.cancel();
107}
108
109
f833126b
CH
110// -----------------------------------------------------------------------------
111// LOAD / SAVE
112// -----------------------------------------------------------------------------
113
96779587
CH
114void DnsCache::schedule_save(const boost::system::error_code &error)
115{
116 // just in case: ensure SaveTimer is cancelled
117 SaveTimer.cancel(); // (will do nothing if already expired/cancelled)
118
119 if ( error == boost::asio::error::operation_aborted ) // cancelled
120 {
49b82a1d
CH
121 GlobalLogger.info() << "DnsCache: SaveTimer was cancelled "
122 << "--> no save and no re-schedule of saving!";
96779587
CH
123 return;
124 }
125 else if (error)
126 {
49b82a1d
CH
127 GlobalLogger.info() << "DnsCache: Received error " << error
128 << " in schedule_save "
129 << "--> no save now but re-schedule saving";
96779587
CH
130 }
131 else
132 save_to_cachefile();
133
134 // schedule next save
135 (void) SaveTimer.expires_from_now( seconds( Config::SaveTimerSeconds ) );
136 SaveTimer.async_wait( bind( &DnsCache::schedule_save, this,
137 boost::asio::placeholders::error ) );
138}
139
140void DnsCache::save_to_cachefile()
141{
142 if (!HasChanged)
ad83004d 143 GlobalLogger.info() << "DnsCache: skip saving because has not changed";
946356e1 144 else if (CacheFile.empty())
49b82a1d 145 GlobalLogger.info()
ad83004d 146 << "DnsCache: skip saving because file name empty!";
8d26221d 147 else if (CacheFile == DoNotUseCacheFile)
26b0f687
CH
148 GlobalLogger.info() << "DnsCache: configured not to use cache file";
149 else
946356e1 150 {
26b0f687
CH
151 try
152 {
153 std::ofstream ofs( CacheFile.c_str() );
154 boost::archive::xml_oarchive oa(ofs);
155 //oa << boost::serialization::make_nvp("IpCache", IpCache);
156 //oa << boost::serialization::make_nvp("CnameCache", CnameCache);
157 oa & BOOST_SERIALIZATION_NVP(IpCache);
158 oa & BOOST_SERIALIZATION_NVP(CnameCache);
8d26221d
CH
159 GlobalLogger.info() << "DnsCache: saved to cache file "
160 << CacheFile;
26b0f687
CH
161
162 HasChanged = false;
163 }
164 catch (std::exception &exc)
165 {
c1abff61 166 GlobalLogger.warning() << "DnsCache: Saving failed: " << exc.what();
26b0f687 167 }
946356e1 168 }
96779587
CH
169}
170
96779587
CH
171void DnsCache::load_from_cachefile()
172{
946356e1 173 if (CacheFile.empty())
49b82a1d 174 GlobalLogger.info()
8d26221d
CH
175 << "DnsCache: cannot load because cache file name is empty!";
176 else if (CacheFile == DoNotUseCacheFile)
26b0f687 177 GlobalLogger.info() << "DnsCache: configured not to use cache file";
946356e1 178 else if ( !I2n::file_exists(CacheFile) )
ad83004d 179 GlobalLogger.warning() << "DnsCache: cannot load because cache file "
946356e1 180 << CacheFile << " does not exist!";
26b0f687 181 else
946356e1 182 {
26b0f687
CH
183 try
184 {
185 std::ifstream ifs( CacheFile.c_str() );
186 boost::archive::xml_iarchive ia(ifs);
946356e1 187
26b0f687
CH
188 ia & BOOST_SERIALIZATION_NVP(IpCache);
189 ia & BOOST_SERIALIZATION_NVP(CnameCache);
190 GlobalLogger.info() << "DnsCache: loaded from file " << CacheFile;
191 }
192 catch (boost::archive::archive_exception &exc)
193 {
8d26221d
CH
194 GlobalLogger.warning()
195 << "DnsCache: archive exception loading from " << CacheFile
196 << ": " << exc.what();
26b0f687
CH
197 }
198 catch (std::exception &exc)
199 {
200 GlobalLogger.warning() << "DnsCache: exception while loading from "
201 << CacheFile << ": " << exc.what();
202 }
946356e1 203 }
96779587
CH
204}
205
26b0f687 206
f833126b
CH
207// -----------------------------------------------------------------------------
208// UPDATE
209// -----------------------------------------------------------------------------
210
26b0f687
CH
211// warn if hostname is empty and remove trailing dot
212std::string DnsCache::key_for_hostname(const std::string &hostname) const
96779587 213{
26b0f687
CH
214 if (hostname.empty())
215 {
49b82a1d 216 GlobalLogger.info() << "DnsCache: empty host!";
26b0f687
CH
217 return "";
218 }
219
220 // check whether last character is a dot
221 if (hostname.rfind('.') == hostname.length()-1)
222 return hostname.substr(0, hostname.length()-1);
223 else
224 return hostname;
96779587
CH
225}
226
227
ad83004d 228void DnsCache::update(const std::string &hostname,
26b0f687 229 const HostAddressVec &new_ips)
ad83004d 230{
26b0f687
CH
231 std::string key = key_for_hostname(hostname);
232 if ( !get_cname(hostname).Host.empty() )
233 { // ensure that there is never IP and CNAME for the same host
49b82a1d 234 GlobalLogger.info() << "DnsCache: Saving IPs for " << key
26b0f687
CH
235 << " removes CNAME to " << get_cname(hostname).Host << "!";
236 update(hostname, Cname()); // overwrite with "empty" cname
237 }
f833126b
CH
238 // ensure min ttl of MinTimeBetweenResolves
239 HostAddressVec ips_checked;
240 BOOST_FOREACH( const HostAddress &addr, new_ips )
241 {
242 if ( addr.get_ttl().get_value() < MinTimeBetweenResolves )
243 {
244 GlobalLogger.info() << "DnsCache: Correcting TTL of IP for "
245 << hostname << " from " << addr.get_ttl().get_value() << "s to "
246 << MinTimeBetweenResolves << "s because was too short";
247 ips_checked.push_back( HostAddress( addr.get_ip(),
248 MinTimeBetweenResolves) );
249 }
250 else
251 ips_checked.push_back(addr);
252 }
253
f636ba88
CH
254 stringstream log_temp;
255 log_temp << "DnsCache: update IPs for " << key << " to "
256 << ips_checked.size() << "-list: ";
257 BOOST_FOREACH( const HostAddress &ip, ips_checked )
258 log_temp << ip.get_ip() << ", ";
259 GlobalLogger.notice() << log_temp.str();
f833126b
CH
260
261 IpCache[key] = ips_checked;
ad83004d
CH
262 HasChanged = true;
263}
264
265
dbe986b9 266void DnsCache::update(const std::string &hostname,
26b0f687 267 const Cname &cname)
ad83004d 268{
26b0f687
CH
269 std::string key = key_for_hostname(hostname);
270 if ( !get_ips(hostname).empty() )
271 { // ensure that there is never IP and CNAME for the same host
49b82a1d 272 GlobalLogger.info() << "DnsCache: Saving CNAME for " << key
26b0f687
CH
273 << " removes " << get_ips(hostname).size() << " IPs for same host!";
274 update(hostname, HostAddressVec()); // overwrite with empty IP list
ad83004d 275 }
26b0f687
CH
276
277 // remove possible trailing dot from cname
278 Cname to_save = Cname(key_for_hostname(cname.Host),
279 cname.Ttl);
280
f833126b
CH
281 // ensure min ttl of MinTimeBetweenResolves
282 if ( to_save.Ttl.get_value() < MinTimeBetweenResolves )
283 {
284 GlobalLogger.info() << "DnsCache: Correcting TTL of CNAME of "
285 << hostname << " from " << to_save.Ttl.get_value() << "s to "
286 << MinTimeBetweenResolves << "s because was too short";
287 to_save.Ttl = TimeToLive(MinTimeBetweenResolves);
288 }
289
f636ba88 290 GlobalLogger.notice() << "DnsCache: update CNAME for " << key
26b0f687
CH
291 << " to " << to_save.Host;
292 CnameCache[key] = to_save;
293 HasChanged = true;
ad83004d
CH
294}
295
296
f833126b
CH
297// -----------------------------------------------------------------------------
298// RETRIEVAL
299// -----------------------------------------------------------------------------
300
23f51766
CH
301/**
302 * @returns empty list if no (up to date) ips for hostname in cache
303 */
dbe986b9
CH
304HostAddressVec DnsCache::get_ips(const std::string &hostname,
305 const bool check_up_to_date)
96779587 306{
26b0f687
CH
307 std::string key = key_for_hostname(hostname);
308 HostAddressVec result = IpCache[key];
dbe986b9
CH
309 if (check_up_to_date)
310 {
311 HostAddressVec result_up_to_date;
23f51766
CH
312 uint32_t threshold = static_cast<uint32_t>(
313 DnsMaster::get_instance()->get_resolved_ip_ttl_threshold() );
26b0f687 314 uint32_t updated_ttl;
dbe986b9
CH
315 BOOST_FOREACH( const HostAddress &addr, result )
316 {
26b0f687
CH
317 updated_ttl = addr.get_ttl().get_updated_value();
318 if (updated_ttl > threshold)
dbe986b9 319 result_up_to_date.push_back(addr);
26b0f687
CH
320 else
321 GlobalLogger.debug() << "DnsCache: do not return "
322 << addr.get_ip().to_string() << " since TTL "
323 << updated_ttl << "s is out of date (thresh="
324 << threshold << "s)";
dbe986b9 325 }
dbe986b9
CH
326 result = result_up_to_date;
327 }
2a4dde8b 328 /*GlobalLogger.debug() << "DnsCache: request IPs for " << key
26b0f687
CH
329 << " --> " << result.size() << "-list";
330 BOOST_FOREACH( const HostAddress &addr, result )
331 GlobalLogger.debug() << "DnsCache: " << addr.get_ip().to_string()
fd62d09f 332 << " (TTL " << addr.get_ttl().get_updated_value()
2a4dde8b 333 << "s)"; */
dbe986b9 334 return result;
96779587
CH
335}
336
23f51766
CH
337/**
338 * @returns empty cname if no (up to date cname) for hostname in cache
339 */
340Cname DnsCache::get_cname(const std::string &hostname,
341 const bool check_up_to_date)
ad83004d 342{
26b0f687
CH
343 std::string key = key_for_hostname(hostname);
344 Cname result_obj = CnameCache[key];
2a4dde8b 345 /*GlobalLogger.debug() << "DnsCache: request CNAME for " << key
fd62d09f 346 << " --> \"" << result_obj.Host << "\" (TTL "
2a4dde8b 347 << result_obj.Ttl.get_updated_value() << "s)";*/
23f51766
CH
348 if (result_obj.Host.empty())
349 return result_obj;
350
351 else if (check_up_to_date)
dbe986b9 352 {
26b0f687
CH
353 if ( result_obj.Ttl.get_updated_value() > static_cast<uint32_t>(
354 DnsMaster::get_instance()->get_resolved_ip_ttl_threshold()) )
23f51766 355 return result_obj;
dbe986b9
CH
356 else
357 {
23f51766
CH
358 GlobalLogger.debug() << "DnsCache: CNAME is out of date";
359 return Cname(); // same as if there was no cname for hostname
dbe986b9
CH
360 }
361 }
362 else
23f51766 363 return result_obj;
ad83004d
CH
364}
365
dbe986b9
CH
366// underlying assumption in this function: for a hostname, the cache has either
367// a list of IPs saved or a cname saved, but never both
368HostAddressVec DnsCache::get_ips_recursive(const std::string &hostname,
369 const bool check_up_to_date)
e18c1337
CH
370{
371 std::string current_host = hostname;
23f51766 372 Cname current_cname;
00c81aa0 373 HostAddressVec result = get_ips(current_host, check_up_to_date);
dbe986b9 374 int n_recursions = 0;
23f51766 375 uint32_t min_cname_ttl = 0xffff; // largest possible unsigned 4-byte value
cd71d095
CH
376 int max_recursion_count = DnsMaster::get_instance()
377 ->get_max_recursion_count();
e18c1337
CH
378 while ( result.empty() )
379 {
23f51766 380 current_cname = get_cname(current_host, check_up_to_date);
8d26221d 381 if (current_cname.Host.empty())
e18c1337 382 break;
8d26221d
CH
383
384 current_host = key_for_hostname(current_cname.Host);
cd71d095 385 if (++n_recursions >= max_recursion_count)
dbe986b9 386 {
49b82a1d
CH
387 GlobalLogger.info() << "DnsCache: reached recursion limit of "
388 << n_recursions << " in recursive IP retrieval of "
389 << hostname << "!";
dbe986b9
CH
390 break;
391 }
e18c1337 392 else
23f51766 393 {
fd62d09f
CH
394 min_cname_ttl = min(min_cname_ttl,
395 current_cname.Ttl.get_updated_value());
dbe986b9 396 result = get_ips(current_host, check_up_to_date);
23f51766
CH
397 }
398 }
399
400 GlobalLogger.debug() << "DnsCache: recursive IP retrieval resulted in "
401 << result.size() << "-list after " << n_recursions
402 << " recursions";
403
404 // adjust ttl to min of ttl and min_cname_ttl
405 if (n_recursions > 0)
406 {
407 TimeToLive cname_ttl(min_cname_ttl);
408
409 BOOST_FOREACH( HostAddress &addr, result )
410 {
fd62d09f 411 if (addr.get_ttl().get_updated_value() > min_cname_ttl)
8d26221d 412 {
2a4dde8b 413 //GlobalLogger.debug() << "DnsCache: using shorter CNAME TTL";
23f51766 414 addr.set_ttl(cname_ttl);
8d26221d 415 }
23f51766 416 }
e18c1337 417 }
23f51766 418
e18c1337
CH
419 return result;
420}
ad83004d 421
23f51766 422/**
9d1b2726
CH
423 * from a list of CNAMEs find the first one that is out of date or empty
424 *
425 * returns the hostname that is out of date or empty if all CNAMEs are
426 * up-to-date
23f51766
CH
427 *
428 * required in ResolverBase::get_skipper
23f51766
CH
429 */
430std::string DnsCache::get_first_outdated_cname(const std::string &hostname,
431 const uint32_t ttl_thresh)
432{
8d26221d 433 std::string first_outdated = hostname;
23f51766
CH
434 Cname cname;
435 int n_recursions = 0;
cd71d095
CH
436 int max_recursion_count = DnsMaster::get_instance()
437 ->get_max_recursion_count();
23f51766
CH
438 while (true)
439 {
cd71d095 440 if (++n_recursions >= max_recursion_count)
23f51766 441 {
49b82a1d
CH
442 GlobalLogger.info() << "DnsCache: reached recursion limit of "
443 << n_recursions << " in search of outdated CNAMEs for "
444 << hostname << "!";
9d1b2726
CH
445 return first_outdated; // not really out of date but currently
446 } // our best guess
23f51766 447
8d26221d
CH
448 cname = get_cname(first_outdated);
449 if (cname.Host.empty())
9d1b2726
CH
450 // reached end of cname list --> everything was up-to-date
451 return "";
23f51766
CH
452 else if (cname.Ttl.get_updated_value() > ttl_thresh)
453 // cname is up to date --> continue looking
8d26221d 454 first_outdated = cname.Host;
23f51766 455 else
9d1b2726
CH
456 // cname is out of date --> return its target
457 return cname.Host;
23f51766 458 }
9d1b2726
CH
459 // reach this point only if cname chain does not end with an IP
460 // --> all are up-to-date
461 return "";
23f51766 462}