b0b2a84323d460ed00a91c85665995dda53e266a
[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 use_webcheck If true: Determine IP via web check
184  * @param changed_to_online Indicates if we just went online
185  * @param wan_override_ip Override automatic WAN IP detection if not empty
186  * @return A string representation of the actual IP in dotted format or an empty string if something went wrong.
187  */
188 string IPAddrHelper::get_actual_ip( bool use_webcheck, bool changed_to_online, const std::string &wan_override_ip )
189 {
190     string ip;
191
192     if ( !WebcheckIpUrl.empty() && use_webcheck )
193         ip = webcheck_ip(changed_to_online);
194     else if (!wan_override_ip.empty())
195         ip = wan_override_ip;
196     else
197         ip = get_local_wan_nic_ip();
198
199     return ip;
200 }
201
202
203 /**
204  * Get the IP address of the local wan interface if there is one.
205  * @return The IP address of the wan interface or an empty string if something went wrong.
206  */
207 string IPAddrHelper::get_local_wan_nic_ip() const
208 {
209     struct ifaddrs *if_addr_struct, *ifa;
210     unsigned short address_family;
211     int ret_val;
212     char ip_addr_buff[NI_MAXHOST];
213     list<string> external_ipv4_addresses;
214     list<string> external_ipv6_addresses;
215
216     // Get linked list of all interface addresses.
217     if ( getifaddrs(&if_addr_struct) == -1 )
218     {
219         Log->print_error_getting_local_wan_ip("getifaddrs", strerror(errno));
220         freeifaddrs(if_addr_struct);
221         return "";
222     }
223
224     // Iterate through the linked list.
225     for ( ifa = if_addr_struct; ifa != NULL; ifa = ifa->ifa_next)
226     {
227         // Skip interfaces without IP addresses
228         if (!ifa->ifa_addr)
229             continue;
230
231         // Get the address family of the actual address.
232         address_family = ifa->ifa_addr->sa_family;
233
234         // If it is an IPv4 then process further.
235         if ( address_family == AF_INET )
236         {
237             // Translate the address to a protocol independent representation (dottet format). Copy address into ip_addr_buff.
238             ret_val = getnameinfo(ifa->ifa_addr, (socklen_t)sizeof(struct sockaddr_in), ip_addr_buff, NI_MAXHOST, NULL, 0, NI_NUMERICHOST);
239             if ( ret_val != 0 )
240             {
241                 Log->print_error_getting_local_wan_ip("getnameinfo", gai_strerror(ret_val));
242                 freeifaddrs(if_addr_struct);
243                 return "";
244             }
245
246             // Generate IPv4 string out of char array.
247             string ipv4_addr(ip_addr_buff);
248
249             Log->print_own_ipv4(ipv4_addr, Hostname);
250
251             // Test if it is a local address.
252             if ( !is_local_ipv4(ipv4_addr) )
253                 external_ipv4_addresses.push_back(ipv4_addr);
254             else
255                 Log->print_ip_is_local(ipv4_addr);
256         }
257         // If it is an IPv6 address and IPv6 is enabled then process further.
258         else if ( (address_family == AF_INET6) && (UseIPv6) )
259         {
260             // Translate the address to a protocol independent representation (dottet format). Copy address into ip_addr_buff.
261             ret_val = getnameinfo(ifa->ifa_addr, (socklen_t)sizeof(struct sockaddr_in6), ip_addr_buff, NI_MAXHOST, NULL, 0, NI_NUMERICHOST);
262             if ( ret_val != 0 )
263             {
264                 Log->print_error_getting_local_wan_ip("getnameinfo", gai_strerror(ret_val));
265                 freeifaddrs(if_addr_struct);
266                 return "";
267             }
268
269             // Generate IPv6 string out of char array.
270             string ipv6_addr(ip_addr_buff);
271
272             Log->print_own_ipv6(ipv6_addr, Hostname);
273
274             // Test if it is a local address.
275             if ( !is_local_ipv6(ipv6_addr) )
276                 external_ipv6_addresses.push_back(ipv6_addr);
277             else
278                 Log->print_ip_is_local(ipv6_addr);
279         }
280     }
281     freeifaddrs(if_addr_struct);
282
283     // Return the first element in IPv6 list if IPv6 is enabled, otherwise return first element in IPv4 list.
284     if ( (UseIPv6) && (!external_ipv6_addresses.empty()) )
285         return external_ipv6_addresses.front();
286     else if ( !external_ipv4_addresses.empty() )
287         return external_ipv4_addresses.front();
288
289     return "";
290 }
291
292
293 /**
294  * Get the actual IP of the given host through a DNS query.
295  * @param _hostname The hostname for the dns lookup, if empty string, then perform a dns lookup to the local hostname.
296  * @return A string representation of the actual IP in dotted format or an empty string if something went wrong.
297  */
298 string IPAddrHelper::dns_query(const string& _hostname) const
299 {
300     list<string> external_ipv4_addresses;
301     list<string> external_ipv6_addresses;
302
303     // Init the hostname with the given _hostname or with local Hostname if empty.
304     string hostname = Hostname;
305     if ( !_hostname.empty() )
306         hostname = _hostname;
307
308     try
309     {
310         // BOOST asio isn't the simplest way to perform a DNS lookup, but it works just fine.
311         net::io_service io_service;
312         net::ip::tcp::resolver resolver(io_service);
313
314         // Define the DNS query.
315         net::ip::tcp::resolver::query query(hostname, "0", net::ip::resolver_query_base::address_configured | net::ip::resolver_query_base::all_matching);
316
317         // Perform the DNS query.
318         net::ip::tcp::resolver::iterator endpoint_iterator = resolver.resolve(query);
319         net::ip::tcp::resolver::iterator end;
320
321         // Iterate through the returned IP addresses.
322         while(endpoint_iterator != end)
323         {
324             // Get the IP address out of the endpoint iterator.
325             net::ip::address ip;
326             ip = endpoint_iterator->endpoint().address();
327
328             // Test if it is a IPv4 address.
329             if ( ip.is_v4() )
330             {
331                 // Get the string representation.
332                 string ipv4_addr = ip.to_string();
333
334                 Log->print_own_ipv4(ipv4_addr, hostname);
335
336                 // If it is not a local address then push it in the external ipv4 address list.
337                 //if ( !is_local_ipv4(ipv4_addr) )
338                     external_ipv4_addresses.push_back(ipv4_addr);
339                 //else
340                 //    Log->print_ip_is_local(ipv4_addr);
341             }
342             // Test if it is a IPv6 address and if IPv6 is enabled.
343             else if ( (ip.is_v6()) && (UseIPv6) )
344             {
345                 // Get the string representation.
346                 string ipv6_addr = ip.to_string();
347
348                 Log->print_own_ipv6(ipv6_addr, hostname);
349
350                 // If it is not a local address then push it in the external ipv6 address list.
351                 //if ( !is_local_ipv6(ipv6_addr) )
352                     external_ipv6_addresses.push_back(ipv6_addr);
353                 //else
354                 //    Log->print_ip_is_local(ipv6_addr);
355             }
356             endpoint_iterator++;
357         }
358         io_service.reset();
359     }
360     catch(const exception& e)
361     {
362         Log->print_error_hostname_to_ip(e.what(),hostname);
363         return "";
364     }
365
366     // Return the first element in IPv6 list if IPv6 is enabled, otherwise return first element in IPv4 list.
367     if ( (UseIPv6) && (!external_ipv6_addresses.empty()) )
368         return external_ipv6_addresses.front();
369     else if ( !external_ipv4_addresses.empty() )
370         return external_ipv4_addresses.front();
371
372     // Could not get a external IP address, so return empty string.
373     return "";
374 }
375
376
377 /**
378  * Get the actual IP of this host through a IP webcheck URL.
379  * @param changed_to_online Indicates if we just went online
380  * @return A string representation of the actual IP in dotted format or an empty string if something went wrong.
381  */
382 string IPAddrHelper::webcheck_ip(bool changed_to_online)
383 {
384     // Init IPAddress with a empty string.
385     string ip_addr = "";
386
387     // Get the current time.
388     time_t current_time = time(NULL);
389
390     // Test if webcheck is allowed due to webcheck_interval.
391     // Ignored if we just went online.
392     if ( !changed_to_online && (LastWebcheck + ((time_t)WebcheckInterval*60)) >= current_time )
393     {
394         // Webcheck not allowed, log it and return empty string.
395         Log->print_webcheck_exceed_interval( LastWebcheck, (WebcheckInterval*60), current_time );
396         return "";
397     }
398
399     // Init CURL buffers
400     string curl_writedata_buff;
401     char curl_err_buff[CURL_ERROR_SIZE];
402     int curl_err_code=1;
403
404     // Init URL List
405     list<string> url_list;
406     url_list.push_back(WebcheckIpUrl);
407     url_list.push_back(WebcheckIpUrlAlt);
408     string actual_url;
409
410     // Init CURL
411     CURL * curl_easy_handle = init_curl(curl_writedata_buff,curl_err_buff);
412     if ( curl_easy_handle == NULL )
413     {
414         return "";
415     }
416
417     // Set the LastWebcheck time to current time.
418     LastWebcheck = current_time;
419
420     // If perform_curl_operation returns a connection problem indicating return code, try the next ip webcheck url if there is one.
421     while ( (curl_err_code == 1) && (url_list.size() != 0) && (url_list.front() != "") )
422     {
423         // Set URL
424         actual_url = url_list.front();
425         url_list.pop_front();
426         if (set_curl_url(curl_easy_handle,actual_url) == CURLE_OK )
427         {
428             // Perform curl operation, err_code of 1 indicated connection problem, so try next url.
429             curl_err_code = perform_curl_operation(curl_easy_handle, curl_err_buff, actual_url);
430         }
431     }
432
433     // Cleanup CURL handle
434     curl_easy_cleanup(curl_easy_handle);
435
436     // If curl_err_code is not 0, the ip couldn't be determined through any configured webcheck url.
437     if ( curl_err_code != 0 )
438     {
439         // Log it and return the empty string.
440         Log->print_webcheck_no_ip();
441         return "";
442     }
443
444     // Log the received curl data.
445     Log->print_received_curl_data(curl_writedata_buff);
446
447     // Try to parse a IPv4 address out of the received data.
448     ip_addr = parse_ipv4(curl_writedata_buff);
449
450     // TODO: Parsing of IPv6 address out of received curl data via webcheck IP.
451
452     if ( !ip_addr.empty() )
453     {
454         // Got a IPv4 address out of the received data.
455         Log->print_own_ipv4(ip_addr, Hostname);
456     }
457     else
458     {
459         return "";
460     }
461
462     // If IP is within a private range then return ""
463     if ( is_local_ipv4(ip_addr) )
464     {
465         Log->print_ip_is_local(ip_addr);
466         return "";
467     }
468
469     // Return the parsed IPAddress.
470     return ip_addr;
471 }
472
473
474 /**
475  * Tries to find a IPv4 Address in dottet format in a given string and returns the IP-Address found.
476  * @param data The string data to search in for a valid IPv4-Address.
477  * @return The IP Address found or an empty string.
478  */
479 string IPAddrHelper::parse_ipv4(const string& data) const
480 {
481     string ip = "";
482
483     // Regex for ipv4 address in dottet format
484     boost::regex expr("([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})");
485
486     boost::smatch matches;
487
488     if ( boost::regex_search(data,matches,expr) )
489     {
490         ip = matches[1];
491         Log->print_regex_match(expr.str(),ip);
492     }
493     else
494     {
495         Log->print_regex_ip_not_found(data);
496     }
497
498     return ip;
499 }
500
501
502 /**
503  * Performs the curl operation.
504  * @param curl_easy_handle The initialized and configured curl handle.
505  * @param curl_err_buff The pointer to the curl error buffer to get error messages from.
506  * @param actual_url The actual configured URL.
507  * @return 0 if all is fine, 1 if an connection problem to the configured url occurs, -1 on other errors.
508  */
509 int IPAddrHelper::perform_curl_operation(CURL * curl_easy_handle, const char* curl_err_buff, const string& actual_url) const
510 {
511     CURLcode curl_err_code;
512     if ( (curl_err_code = curl_easy_perform(curl_easy_handle) ) != CURLE_OK )
513     {
514         // CURL error occured
515         if ( (curl_err_code == CURLE_COULDNT_CONNECT) || (curl_err_code == CURLE_OPERATION_TIMEOUTED) || (curl_err_code == CURLE_COULDNT_RESOLVE_HOST) )
516         {
517             // In case of connection problems we should return 1, that the fallback url will be used.
518             Log->print_webcheck_url_connection_problem(curl_err_buff, actual_url);
519             return 1;
520         }
521         else
522         {
523             // other error
524             Log->print_webcheck_error(curl_err_buff, actual_url);
525             return -1;
526         }
527     }
528     // Operation performed without any problems
529     return 0;
530 }
531
532
533 /**
534  * Sets a url to the easy curl handle
535  * @param curl_easy_handle The easy curl handle to set the url for.
536  * @param url The url to set.
537  * @return CURLcode Curl Error code, CURLE_OK if everything is right.
538  */
539 CURLcode IPAddrHelper::set_curl_url(CURL * curl_easy_handle, const string& url) const
540 {
541     CURLcode curl_error = CURLE_OK;
542
543     if ( curl_easy_handle != NULL )
544     {
545         curl_error = curl_easy_setopt(curl_easy_handle,CURLOPT_URL,url.c_str());
546         if ( curl_error != CURLE_OK )
547         {
548             // Some options could not be set, so destroy the CURL handle.
549             Log->print_curl_error_init("Could not set CURL URL properly.",curl_error);
550         }
551     }
552     return curl_error;
553 }
554
555
556 /**
557  * Initialized curl easy handle with a few options.
558  * @param curl_writedata_buff Reference to a string wich will be filled with the curl result
559  * @param curl_err_buff A pointer to an char array which will be filled with error message.
560  * @return A pointer to the easy curl handle.
561  */
562 CURL * IPAddrHelper::init_curl(string& curl_writedata_buff,char* curl_err_buff) const
563 {
564     ostringstream user_agent_stream;
565     user_agent_stream << "Intra2net AG - Bullet Proof DYNDNS Daemon - " << MAJOR_VERSION << "." << MINOR_VERSION;
566     string user_agent = user_agent_stream.str();
567
568     CURL *curl_easy_handle = curl_easy_init();
569     if ( curl_easy_handle == NULL )
570     {
571         // something went wrong.
572         Log->print_curl_error_init("Could not initialize CURL object.",CURLE_FAILED_INIT);
573         return NULL;
574     }
575
576     CURLcode curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_NOPROGRESS,1);
577     if ( curlError == CURLE_OK)
578         curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_CONNECTTIMEOUT,5);
579     if ( curlError == CURLE_OK)
580         curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_TIMEOUT,10);
581     if ( curlError == CURLE_OK)
582         curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_BUFFERSIZE,1024);
583     if ( curlError == CURLE_OK)
584         curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_ERRORBUFFER,curl_err_buff);
585     if ( curlError == CURLE_OK)
586         curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_WRITEFUNCTION,http_receive);
587     if ( curlError == CURLE_OK)
588         curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_WRITEDATA,&curl_writedata_buff);
589     if ( curlError == CURLE_OK)
590         curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_USERAGENT,user_agent.c_str());
591
592     if ( !Proxy.empty() )
593     {
594         if ( curlError == CURLE_OK)
595             curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_PROXY,Proxy.c_str());
596         if ( curlError == CURLE_OK)
597             curlError = curl_easy_setopt(curl_easy_handle,CURLOPT_PROXYPORT,ProxyPort);
598     }
599
600     if ( curlError != CURLE_OK )
601     {
602         // Some options could not be set, so destroy the CURL handle.
603         Log->print_curl_error_init("Could not set CURL options properly.",curlError);
604         curl_easy_cleanup(curl_easy_handle);
605         curl_easy_handle = NULL;
606     }
607
608     return curl_easy_handle;
609 }
610
611
612 /**
613  * Callback Function is called every time CURL is receiving data from HTTPS-Server and will copy all received Data to the given stream pointer
614  * @param inBuffer Pointer to input.
615  * @param size How many mem blocks are received
616  * @param nmemb size of each memblock
617  * @param outBuffer Pointer to output stream.
618  * @return The size received.
619  */
620 size_t IPAddrHelper::http_receive( void *inBuffer, size_t size, size_t nmemb, string *outBuffer )
621 {
622     outBuffer->append(static_cast<char *>(inBuffer), size*nmemb);
623     return (size*nmemb);
624 } //lint !e818
625
626
627 /**
628  * Get member LastWebcheck
629  * @return LastWebcheck
630  */
631 time_t IPAddrHelper::get_last_webcheck() const
632 {
633     return LastWebcheck;
634 }