16359dd332ae798b24267fc7124b6e955523c55e
[bpdyndnsd] / src / ip_addr_helper.cpp
1 /** @file
2  * @brief IPHelper class implementation. This class represents a Helper to get the actual IP.
3  *
4  *
5  *
6  * @copyright Intra2net AG
7  * @license GPLv2
8 */
9
10 #include "ip_addr_helper.hpp"
11 #include <boost/asio.hpp>
12 #include <boost/regex.hpp>
13 #include <arpa/inet.h>
14 #include <sys/socket.h>
15 #include <netdb.h>
16 #include <ifaddrs.h>
17 #include "version_info.h"
18
19
20 using namespace std;
21
22 namespace net = boost::asio;
23
24 /**
25  * Default constructor.
26  */
27 IPAddrHelper::IPAddrHelper()
28     : Log(new Logger)
29     , WebcheckInterval(0)
30     , LastWebcheck(0)
31     , ProxyPort(0)
32     , UseIPv6(false)
33 {
34 }
35
36
37 /**
38  * Constructor.
39  */
40 IPAddrHelper::IPAddrHelper(const Logger::Ptr _log, const string& _webcheck_url, const string& _webcheck_url_alt, const int _webcheck_interval, const time_t _last_webcheck ,const bool _use_ipv6, const string& _proxy, const int _proxy_port)
41     : Log(_log)
42     , WebcheckIpUrl(_webcheck_url)
43     , WebcheckIpUrlAlt(_webcheck_url_alt)
44     , WebcheckInterval(_webcheck_interval)
45     , LastWebcheck(_last_webcheck)
46     , Proxy(_proxy)
47     , ProxyPort(_proxy_port)
48     , UseIPv6(_use_ipv6)
49 {
50     Hostname = net::ip::host_name();
51     Log->print_hostname(Hostname);
52 }
53
54
55 /**
56  * Default destructor
57  */
58 IPAddrHelper::~IPAddrHelper()
59 {
60 }
61
62
63 /**
64  * Tests if a given IP is a local IPv6 address
65  * @param ip The IP to test
66  * @return true if given IP is local, false if not.
67  */
68 bool IPAddrHelper::is_local_ipv6(const string ip) const
69 {
70     // IPv6 any
71     boost::regex expr_any_ipv6("^::$");
72
73     // IPV6 loopback
74     boost::regex expr_loopback_ipv6("^::1$");
75
76     // IPV6 local unicast address
77     boost::regex expr_local_unicast_ipv6("^fc00:");
78
79     // IPV6 link local
80     boost::regex expr_link_local_ipv6("^fe[8,9,a,b]{1}");
81
82     // IPV6 site local
83     boost::regex expr_site_local_ipv6("^fe[c,d,e,f]{1}");
84
85     // It's time to test against the regex patterns
86     if ( boost::regex_search(ip,expr_any_ipv6) )
87     {
88         Log->print_regex_match(expr_any_ipv6.str(),ip);
89         return true;
90     }
91     else if ( boost::regex_search(ip,expr_loopback_ipv6) )
92     {
93         Log->print_regex_match(expr_loopback_ipv6.str(),ip);
94         return true;
95     }
96     else if ( boost::regex_search(ip,expr_local_unicast_ipv6) )
97     {
98         Log->print_regex_match(expr_local_unicast_ipv6.str(),ip);
99         return true;
100     }
101     else if ( boost::regex_search(ip,expr_link_local_ipv6) )
102     {
103         Log->print_regex_match(expr_link_local_ipv6.str(),ip);
104         return true;
105     }
106     else if ( boost::regex_search(ip,expr_site_local_ipv6) )
107     {
108         Log->print_regex_match(expr_site_local_ipv6.str(),ip);
109         return true;
110     }
111
112     return false;
113 }
114
115
116 /**
117  * Tests if a given IP is a local IPv4 address
118  * @param ip The IP to test
119  * @return true if given IP is local, false if not.
120  */
121 bool IPAddrHelper::is_local_ipv4(const string ip) const
122 {
123     // 127.0.0.1
124     boost::regex expr_loopback("127\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}");
125
126     // 192.168.x.x
127     boost::regex expr_192("192\\.168\\.[0-9]{1,3}\\.[0-9]{1,3}");
128
129     // 10.x.x.x
130     boost::regex expr_10("10\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}");
131
132     // 169.254.x.x
133     boost::regex expr_169_254("169\\.254\\.[0-9]{1,3}\\.[0-9]{1,3}");
134
135     // 172.16.x.x -> 172.31.x.x
136     boost::regex expr_172_1("172\\.1[6-9]{1}\\.[0-9]{1,3}\\.[0-9]{1,3}");
137     boost::regex expr_172_2("172\\.2[0-9]{1}\\.[0-9]{1,3}\\.[0-9]{1,3}");
138     boost::regex expr_172_3("172\\.3[0-1]{1}\\.[0-9]{1,3}\\.[0-9]{1,3}");
139
140     // It's time to test against the regex patterns
141     if ( boost::regex_search(ip,expr_loopback) )
142     {
143         Log->print_regex_match(expr_loopback.str(),ip);
144         return true;
145     }
146     else if ( boost::regex_search(ip,expr_192) )
147     {
148         Log->print_regex_match(expr_192.str(),ip);
149         return true;
150     }
151     else if ( boost::regex_search(ip,expr_10) )
152     {
153         Log->print_regex_match(expr_10.str(),ip);
154         return true;
155     }
156     else if ( boost::regex_search(ip,expr_169_254) )
157     {
158         Log->print_regex_match(expr_169_254.str(),ip);
159         return true;
160     }
161     else if ( boost::regex_search(ip,expr_172_1) )
162     {
163         Log->print_regex_match(expr_172_1.str(),ip);
164         return true;
165     }
166     else if ( boost::regex_search(ip,expr_172_2) )
167     {
168         Log->print_regex_match(expr_172_2.str(),ip);
169         return true;
170     }
171     else if ( boost::regex_search(ip,expr_172_3) )
172     {
173         Log->print_regex_match(expr_172_3.str(),ip);
174         return true;
175     }
176
177     return false;
178 }
179
180
181 /**
182  * Get the actual IP of this host through a conventional DNS query or through a IP webcheck URL if configured so.
183  * @param changed_to_online Indicates if we just went online
184  * @return A string representation of the actual IP in dotted format or an empty string if something went wrong.
185  */
186 string IPAddrHelper::get_actual_ip( bool use_webcheck, bool changed_to_online )
187 {
188     string ip;
189
190     if ( !WebcheckIpUrl.empty() && use_webcheck )
191         ip = webcheck_ip(changed_to_online);
192     else
193         ip = get_local_wan_nic_ip();
194
195     return ip;
196 }
197
198
199 /**
200  * Get the IP address of the local wan interface if there is one.
201  * @return The IP address of the wan interface or an empty string if something went wrong.
202  */
203 string IPAddrHelper::get_local_wan_nic_ip() const
204 {
205     struct ifaddrs *if_addr_struct, *ifa;
206     unsigned short address_family;
207     int ret_val;
208     char ip_addr_buff[NI_MAXHOST];
209     list<string> external_ipv4_addresses;
210     list<string> external_ipv6_addresses;
211
212     // Get linked list of all interface addresses.
213     if ( getifaddrs(&if_addr_struct) == -1 )
214     {
215         Log->print_error_getting_local_wan_ip("getifaddrs", strerror(errno));
216         freeifaddrs(if_addr_struct);
217         return "";
218     }
219
220     // Iterate through the linked list.
221     for ( ifa = if_addr_struct; ifa != NULL; ifa = ifa->ifa_next)
222     {
223         // Skip interfaces without IP addresses
224         if (!ifa->ifa_addr)
225             continue;
226
227         // Get the address family of the actual address.
228         address_family = ifa->ifa_addr->sa_family;
229
230         // If it is an IPv4 then process further.
231         if ( address_family == AF_INET )
232         {
233             // Translate the address to a protocol independent representation (dottet format). Copy address into ip_addr_buff.
234             ret_val = getnameinfo(ifa->ifa_addr, (socklen_t)sizeof(struct sockaddr_in), ip_addr_buff, NI_MAXHOST, NULL, 0, NI_NUMERICHOST);
235             if ( ret_val != 0 )
236             {
237                 Log->print_error_getting_local_wan_ip("getnameinfo", gai_strerror(ret_val));
238                 freeifaddrs(if_addr_struct);
239                 return "";
240             }
241
242             // Generate IPv4 string out of char array.
243             string ipv4_addr(ip_addr_buff);
244
245             Log->print_own_ipv4(ipv4_addr, Hostname);
246
247             // Test if it is a local address.
248             if ( !is_local_ipv4(ipv4_addr) )
249                 external_ipv4_addresses.push_back(ipv4_addr);
250             else
251                 Log->print_ip_is_local(ipv4_addr);
252         }
253         // If it is an IPv6 address and IPv6 is enabled then process further.
254         else if ( (address_family == AF_INET6) && (UseIPv6) )
255         {
256             // Translate the address to a protocol independent representation (dottet format). Copy address into ip_addr_buff.
257             ret_val = getnameinfo(ifa->ifa_addr, (socklen_t)sizeof(struct sockaddr_in6), ip_addr_buff, NI_MAXHOST, NULL, 0, NI_NUMERICHOST);
258             if ( ret_val != 0 )
259             {
260                 Log->print_error_getting_local_wan_ip("getnameinfo", gai_strerror(ret_val));
261                 freeifaddrs(if_addr_struct);
262                 return "";
263             }
264
265             // Generate IPv6 string out of char array.
266             string ipv6_addr(ip_addr_buff);
267
268             Log->print_own_ipv6(ipv6_addr, Hostname);
269
270             // Test if it is a local address.
271             if ( !is_local_ipv6(ipv6_addr) )
272                 external_ipv6_addresses.push_back(ipv6_addr);
273             else
274                 Log->print_ip_is_local(ipv6_addr);
275         }
276     }
277     freeifaddrs(if_addr_struct);
278
279     // Return the first element in IPv6 list if IPv6 is enabled, otherwise return first element in IPv4 list.
280     if ( (UseIPv6) && (!external_ipv6_addresses.empty()) )
281         return external_ipv6_addresses.front();
282     else if ( !external_ipv4_addresses.empty() )
283         return external_ipv4_addresses.front();
284
285     return "";
286 }
287
288
289 /**
290  * Get the actual IP of the given host through a DNS query.
291  * @param _hostname The hostname for the dns lookup, if empty string, then perform a dns lookup to the local hostname.
292  * @return A string representation of the actual IP in dotted format or an empty string if something went wrong.
293  */
294 string IPAddrHelper::dns_query(const string& _hostname) const
295 {
296     list<string> external_ipv4_addresses;
297     list<string> external_ipv6_addresses;
298
299     // Init the hostname with the given _hostname or with local Hostname if empty.
300     string hostname = Hostname;
301     if ( !_hostname.empty() )
302         hostname = _hostname;
303
304     try
305     {
306         // BOOST asio isn't the simplest way to perform a DNS lookup, but it works just fine.
307         net::io_service io_service;
308         net::ip::tcp::resolver resolver(io_service);
309
310         // Define the DNS query.
311         net::ip::tcp::resolver::query query(hostname, "0", net::ip::resolver_query_base::address_configured | net::ip::resolver_query_base::all_matching);
312
313         // Perform the DNS query.
314         net::ip::tcp::resolver::iterator endpoint_iterator = resolver.resolve(query);
315         net::ip::tcp::resolver::iterator end;
316
317         // Iterate through the returned IP addresses.
318         while(endpoint_iterator != end)
319         {
320             // Get the IP address out of the endpoint iterator.
321             net::ip::address ip;
322             ip = endpoint_iterator->endpoint().address();
323
324             // Test if it is a IPv4 address.
325             if ( ip.is_v4() )
326             {
327                 // Get the string representation.
328                 string ipv4_addr = ip.to_string();
329
330                 Log->print_own_ipv4(ipv4_addr, hostname);
331
332                 // If it is not a local address then push it in the external ipv4 address list.
333                 //if ( !is_local_ipv4(ipv4_addr) )
334                     external_ipv4_addresses.push_back(ipv4_addr);
335                 //else
336                 //    Log->print_ip_is_local(ipv4_addr);
337             }
338             // Test if it is a IPv6 address and if IPv6 is enabled.
339             else if ( (ip.is_v6()) && (UseIPv6) )
340             {
341                 // Get the string representation.
342                 string ipv6_addr = ip.to_string();
343
344                 Log->print_own_ipv6(ipv6_addr, hostname);
345
346                 // If it is not a local address then push it in the external ipv6 address list.
347                 //if ( !is_local_ipv6(ipv6_addr) )
348                     external_ipv6_addresses.push_back(ipv6_addr);
349                 //else
350                 //    Log->print_ip_is_local(ipv6_addr);
351             }
352             endpoint_iterator++;
353         }
354         io_service.reset();
355     }
356     catch(const exception& e)
357     {
358         Log->print_error_hostname_to_ip(e.what(),hostname);
359         return "";
360     }
361
362     // Return the first element in IPv6 list if IPv6 is enabled, otherwise return first element in IPv4 list.
363     if ( (UseIPv6) && (!external_ipv6_addresses.empty()) )
364         return external_ipv6_addresses.front();
365     else if ( !external_ipv4_addresses.empty() )
366         return external_ipv4_addresses.front();
367
368     // Could not get a external IP address, so return empty string.
369     return "";
370 }
371
372
373 /**
374  * Get the actual IP of this host through a IP webcheck URL.
375  * @param changed_to_online Indicates if we just went online
376  * @return A string representation of the actual IP in dotted format or an empty string if something went wrong.
377  */
378 string IPAddrHelper::webcheck_ip(bool changed_to_online)
379 {
380     // Init IPAddress with a empty string.
381     string ip_addr = "";
382
383     // Get the current time.
384     time_t current_time = time(NULL);
385
386     // Test if webcheck is allowed due to webcheck_interval.
387     // Ignored if we just went online.
388     if ( !changed_to_online && (LastWebcheck + ((time_t)WebcheckInterval*60)) >= current_time )
389     {
390         // Webcheck not allowed, log it and return empty string.
391         Log->print_webcheck_exceed_interval( LastWebcheck, (WebcheckInterval*60), current_time );
392         return "";
393     }
394
395     // Init CURL buffers
396     string curl_writedata_buff;
397     char curl_err_buff[CURL_ERROR_SIZE];
398     int curl_err_code=1;
399
400     // Init URL List
401     list<string> url_list;
402     url_list.push_back(WebcheckIpUrl);
403     url_list.push_back(WebcheckIpUrlAlt);
404     string actual_url;
405
406     // Init CURL
407     CURL * curl_easy_handle = init_curl(curl_writedata_buff,curl_err_buff);
408     if ( curl_easy_handle == NULL )
409     {
410         return "";
411     }
412
413     // Set the LastWebcheck time to current time.
414     LastWebcheck = current_time;
415
416     // If perform_curl_operation returns a connection problem indicating return code, try the next ip webcheck url if there is one.
417     while ( (curl_err_code == 1) && (url_list.size() != 0) && (url_list.front() != "") )
418     {
419         // Set URL
420         actual_url = url_list.front();
421         url_list.pop_front();
422         if (set_curl_url(curl_easy_handle,actual_url) == CURLE_OK )
423         {
424             // Perform curl operation, err_code of 1 indicated connection problem, so try next url.
425             curl_err_code = perform_curl_operation(curl_easy_handle, curl_err_buff, actual_url);
426         }
427     }
428
429     // Cleanup CURL handle
430     curl_easy_cleanup(curl_easy_handle);
431
432     // If curl_err_code is not 0, the ip couldn't be determined through any configured webcheck url.
433     if ( curl_err_code != 0 )
434     {
435         // Log it and return the empty string.
436         Log->print_webcheck_no_ip();
437         return "";
438     }
439
440     // Log the received curl data.
441     Log->print_received_curl_data(curl_writedata_buff);
442
443     // Try to parse a IPv4 address out of the received data.
444     ip_addr = parse_ipv4(curl_writedata_buff);
445
446     // TODO: Parsing of IPv6 address out of received curl data via webcheck IP.
447
448     if ( !ip_addr.empty() )
449     {
450         // Got a IPv4 address out of the received data.
451         Log->print_own_ipv4(ip_addr, Hostname);
452     }
453     else
454     {
455         return "";
456     }
457
458     // If IP is within a private range then return ""
459     if ( is_local_ipv4(ip_addr) )
460     {
461         Log->print_ip_is_local(ip_addr);
462         return "";
463     }
464
465     // Return the parsed IPAddress.
466     return ip_addr;
467 }
468
469
470 /**
471  * Tries to find a IPv4 Address in dottet format in a given string and returns the IP-Address found.
472  * @param data The string data to search in for a valid IPv4-Address.
473  * @return The IP Address found or an empty string.
474  */
475 string IPAddrHelper::parse_ipv4(const string& data) const
476 {
477     string ip = "";
478
479     // Regex for ipv4 address in dottet format
480     boost::regex expr("([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})");
481
482     boost::smatch matches;
483
484     if ( boost::regex_search(data,matches,expr) )
485     {
486         ip = matches[1];
487         Log->print_regex_match(expr.str(),ip);
488     }
489     else
490     {
491         Log->print_regex_ip_not_found(data);
492     }
493
494     return ip;
495 }
496
497
498 /**
499  * Performs the curl operation.
500  * @param curl_easy_handle The initialized and configured curl handle.
501  * @param curl_err_buff The pointer to the curl error buffer to get error messages from.
502  * @param actual_url The actual configured URL.
503  * @return 0 if all is fine, 1 if an connection problem to the configured url occurs, -1 on other errors.
504  */
505 int IPAddrHelper::perform_curl_operation(CURL * curl_easy_handle, const char* curl_err_buff, const string& actual_url) const
506 {
507     CURLcode curl_err_code;
508     if ( (curl_err_code = curl_easy_perform(curl_easy_handle) ) != CURLE_OK )
509     {
510         // CURL error occured
511         if ( (curl_err_code == CURLE_COULDNT_CONNECT) || (curl_err_code == CURLE_OPERATION_TIMEOUTED) || (curl_err_code == CURLE_COULDNT_RESOLVE_HOST) )
512         {
513             // In case of connection problems we should return 1, that the fallback url will be used.
514             Log->print_webcheck_url_connection_problem(curl_err_buff, actual_url);
515             return 1;
516         }
517         else
518         {
519             // other error
520             Log->print_webcheck_error(curl_err_buff, actual_url);
521             return -1;
522         }
523     }
524     // Operation performed without any problems
525     return 0;
526 }
527
528
529 /**
530  * Sets a url to the easy curl handle
531  * @param curl_easy_handle The easy curl handle to set the url for.
532  * @param url The url to set.
533  * @return CURLcode Curl Error code, CURLE_OK if everything is right.
534  */
535 CURLcode IPAddrHelper::set_curl_url(CURL * curl_easy_handle, const string& url) const
536 {
537     CURLcode curl_error = CURLE_OK;
538
539     if ( curl_easy_handle != NULL )
540     {
541         curl_error = curl_easy_setopt(curl_easy_handle,CURLOPT_URL,url.c_str());
542         if ( curl_error != CURLE_OK )
543         {
544             // Some options could not be set, so destroy the CURL handle.
545             Log->print_curl_error_init("Could not set CURL URL properly.",curl_error);
546         }
547     }
548     return curl_error;
549 }
550
551
552 /**
553  * Initialized curl easy handle with a few options.
554  * @param curl_writedata_buff Reference to a string wich will be filled with the curl result
555  * @param curl_err_buff A pointer to an char array which will be filled with error message.
556  * @return A pointer to the easy curl handle.
557  */
558 CURL * IPAddrHelper::init_curl(string& curl_writedata_buff,char* curl_err_buff) const
559 {
560     ostringstream user_agent_stream;
561     user_agent_stream << "Intra2net AG - Bullet Proof DYNDNS Daemon - " << MAJOR_VERSION << "." << MINOR_VERSION;
562     string user_agent = user_agent_stream.str();
563
564     CURL *curl_easy_handle = curl_easy_init();
565     if ( curl_easy_handle == NULL )
566     {
567         // something went wrong.
568         Log->print_curl_error_init("Could not initialize CURL object.",CURLE_FAILED_INIT);
569         return NULL;
570     }
571
572     CURLcode curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_NOPROGRESS,1);
573     if ( curlError == CURLE_OK)
574         curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_CONNECTTIMEOUT,5);
575     if ( curlError == CURLE_OK)
576         curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_TIMEOUT,10);
577     if ( curlError == CURLE_OK)
578         curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_BUFFERSIZE,1024);
579     if ( curlError == CURLE_OK)
580         curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_ERRORBUFFER,curl_err_buff);
581     if ( curlError == CURLE_OK)
582         curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_WRITEFUNCTION,http_receive);
583     if ( curlError == CURLE_OK)
584         curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_WRITEDATA,&curl_writedata_buff);
585     if ( curlError == CURLE_OK)
586         curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_USERAGENT,user_agent.c_str());
587
588     if ( !Proxy.empty() )
589     {
590         if ( curlError == CURLE_OK)
591             curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_PROXY,Proxy.c_str());
592         if ( curlError == CURLE_OK)
593             curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_PROXYPORT,ProxyPort);
594     }
595
596     if ( curlError != CURLE_OK )
597     {
598         // Some options could not be set, so destroy the CURL handle.
599         Log->print_curl_error_init("Could not set CURL options properly.",curlError);
600         curl_easy_cleanup(curl_easy_handle);
601         curl_easy_handle = NULL;
602     }
603
604     return curl_easy_handle;
605 }
606
607
608 /**
609  * Callback Function is called every time CURL is receiving data from HTTPS-Server and will copy all received Data to the given stream pointer
610  * @param inBuffer Pointer to input.
611  * @param size How many mem blocks are received
612  * @param nmemb size of each memblock
613  * @param outBuffer Pointer to output stream.
614  * @return The size received.
615  */
616 size_t IPAddrHelper::http_receive( void *inBuffer, size_t size, size_t nmemb, string *outBuffer )
617 {
618     outBuffer->append(static_cast<char *>(inBuffer), size*nmemb);
619     return (size*nmemb);
620 } //lint !e818
621
622
623 /**
624  * Get member LastWebcheck
625  * @return LastWebcheck
626  */
627 time_t IPAddrHelper::get_last_webcheck() const
628 {
629     return LastWebcheck;
630 }