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