From d24b75960d908f64bb6ce857006056e1b4224016 Mon Sep 17 00:00:00 2001 From: David Kerr Date: Fri, 17 Jul 2020 09:50:32 -0400 Subject: [PATCH 01/16] Add support for curl and option to not validate SSL certificates for getip from web or firewall Check exists not just length --- .gitignore | 1 + ChangeLog.md | 7 + Makefile.am | 5 +- configure.ac | 5 + ddclient.in | 184 +++++++++++++++++- ...nectivity.pl => geturl_connectivity.pl.in} | 24 ++- t/geturl_ssl.pl | 11 ++ 7 files changed, 231 insertions(+), 6 deletions(-) rename t/{geturl_connectivity.pl => geturl_connectivity.pl.in} (79%) diff --git a/.gitignore b/.gitignore index a7f53a8..3eae749 100644 --- a/.gitignore +++ b/.gitignore @@ -17,5 +17,6 @@ release /ddclient.conf /t/*.log /t/*.trs +/t/geturl_connectivity.pl /t/version.pl /test-suite.log diff --git a/ChangeLog.md b/ChangeLog.md index dc98bab..87d6ce5 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -36,6 +36,13 @@ repository history](https://github.com/ddclient/ddclient/commits/master). - `siemens-ss4200`: Siemens SpeedStream 4200 - `thomson-st536v6`: Thomson SpeedTouch 536v6 - `thomson-tg782`: Thomson/Technicolor TG782 + * Added option `-curl` to access network with system Curl command instead + of the Perl built-in IO::Socket classes. + * Added option `-{no}web-ssl-validate` and `-{no}fw-ssl-validate`to provide + option to disable SSL certificate validation. Note that these only apply for + network access when obtaining an IP address with `use=web` or `use=fw` + (any firewall). Network access to Dynamic DNS servers to set or retrieve + IP address will always require certificate validation. ### Bug fixes diff --git a/Makefile.am b/Makefile.am index 636f379..5e5d633 100644 --- a/Makefile.am +++ b/Makefile.am @@ -31,7 +31,8 @@ subst = sed \ -e '1 s|^\#\!.*perl$$|\#\!$(PERL)|g' \ -e 's|@localstatedir[@]|$(localstatedir)|g' \ -e 's|@runstatedir[@]|$(runstatedir)|g' \ - -e 's|@sysconfdir[@]|$(sysconfdir)|g' + -e 's|@sysconfdir[@]|$(sysconfdir)|g' \ + -e 's|@CURL[@]|$(CURL)|g' # Files that will be generated by passing their *.in file through # $(subst). @@ -70,7 +71,6 @@ AM_PL_LOG_FLAGS = -Mstrict -w \ -I'$(abs_top_srcdir)'/t/lib \ -MDevel::Autoflush handwritten_tests = \ - t/geturl_connectivity.pl \ t/geturl_ssl.pl \ t/is-and-extract-ipv4.pl \ t/is-and-extract-ipv6.pl \ @@ -78,6 +78,7 @@ handwritten_tests = \ t/parse_assignments.pl \ t/write_cache.pl generated_tests = \ + t/geturl_connectivity.pl \ t/version.pl TESTS = $(handwritten_tests) $(generated_tests) EXTRA_DIST += $(handwritten_tests) \ diff --git a/configure.ac b/configure.ac index 821f842..cc6a8b6 100644 --- a/configure.ac +++ b/configure.ac @@ -27,6 +27,8 @@ AC_PROG_MKDIR_P AC_PATH_PROG([FIND], [find]) AS_IF([test -z "${FIND}"], [AC_MSG_ERROR(['find' utility not found])]) +AC_PATH_PROG([CURL], [curl]) + AX_WITH_PROG([PERL], perl) AX_PROG_PERL_VERSION([5.10.1], [], [AC_MSG_ERROR([Perl 5.10.1 or newer not found])]) @@ -39,6 +41,7 @@ AC_SUBST([PERL]) m4_foreach_w([_m], [ File::Basename File::Path + File::Temp Getopt::Long IO::Socket::INET Socket @@ -70,6 +73,7 @@ m4_foreach_w([_m], [ HTTP::Response IO::Socket::INET6 IO::Socket::IP + IO::Socket::SSL Scalar::Util Test::MockModule Test::TCP @@ -81,6 +85,7 @@ m4_foreach_w([_m], [ AC_CONFIG_FILES([ Makefile + t/geturl_connectivity.pl t/version.pl ]) AC_OUTPUT diff --git a/ddclient.in b/ddclient.in index b6db09c..acbe532 100755 --- a/ddclient.in +++ b/ddclient.in @@ -24,6 +24,7 @@ use strict; use warnings; use File::Basename; use File::Path qw(make_path); +use File::Temp; use Getopt::Long; use IO::Socket::INET; use Socket qw(AF_INET AF_INET6 PF_INET PF_INET6); @@ -403,6 +404,7 @@ my %variables = ( 'retry' => setv(T_BOOL, 0, 0, 0, undef), 'force' => setv(T_BOOL, 0, 0, 0, undef), 'ssl' => setv(T_BOOL, 0, 0, 0, undef), + 'curl' => setv(T_BOOL, 0, 0, 0, undef), 'ipv6' => setv(T_BOOL, 0, 0, 0, undef), 'syslog' => setv(T_BOOL, 0, 0, 0, undef), 'facility' => setv(T_STRING,0, 0, 'daemon', undef), @@ -432,10 +434,12 @@ my %variables = ( 'if' => setv(T_IF, 0, 0, 'ppp0', undef), 'web' => setv(T_STRING,0, 0, 'dyndns', undef), 'web-skip' => setv(T_STRING,0, 0, '', undef), + 'web-ssl-validate' => setv(T_BOOL, 0, 0, 1, undef), 'fw' => setv(T_ANY, 0, 0, '', undef), 'fw-skip' => setv(T_STRING,0, 0, '', undef), 'fw-login' => setv(T_LOGIN, 0, 0, '', undef), 'fw-password' => setv(T_PASSWD,0, 0, '', undef), + 'fw-ssl-validate' => setv(T_BOOL, 0, 0, 1, undef), 'cmd' => setv(T_PROG, 0, 0, '', undef), 'cmd-skip' => setv(T_STRING,0, 0, '', undef), 'ipv6' => setv(T_BOOL, 0, 0, 0, undef), @@ -812,6 +816,9 @@ my @opt = ( ["ssl", "!", "-{no}ssl : do updates over encrypted SSL connection"], ["ssl_ca_dir", "=s", "-ssl_ca_dir : look in for certificates of trusted certificate authorities (default: auto-detect)"], ["ssl_ca_file", "=s", "-ssl_ca_file : look at for certificates of trusted certificate authorities (default: auto-detect)"], + ["fw-ssl-validate","!", "-{no}fw-ssl-validate : Validate SSL certificate when retrieving IP address from firewall"], + ["web-ssl-validate","!","-{no}web-ssl-validate : Validate SSL certificate when retrieving IP address from web"], + ["curl", "!", "-{no}curl : use curl for network connections"], ["retry", "!", "-{no}retry : retry failed updates"], ["force", "!", "-{no}force : force an update even if the update may be unnecessary"], ["timeout", "=i", "-timeout : when fetching a URL, wait at most seconds for a response"], @@ -2037,10 +2044,15 @@ EOM } import JSON::PP (qw/decode_json encode_json/); } + ###################################################################### ## geturl ###################################################################### sub geturl { + return opt('curl') ? fetch_via_curl(@_) : fetch_via_socket_io(@_); +} + +sub fetch_via_socket_io { my %params = @_; my $proxy = $params{proxy}; my $url = $params{url}; @@ -2127,6 +2139,9 @@ sub geturl { $socket_class = 'IO::Socket::SSL'; $socket_args{SSL_ca_file} = opt('ssl_ca_file') if defined(opt('ssl_ca_file')); $socket_args{SSL_ca_path} = opt('ssl_ca_dir') if defined(opt('ssl_ca_dir')); + $socket_args{SSL_verify_mode} = ($params{ssl_validate} // 1) + ? IO::Socket::SSL->SSL_VERIFY_PEER + : IO::Socket::SSL->SSL_VERIFY_NONE; } elsif ($globals{'ipv6'} || $ipversion eq '6') { load_ipv6_support; $socket_class = 'IO::Socket::INET6'; @@ -2207,6 +2222,166 @@ sub geturl { return $reply; } +###################################################################### +## curl_cmd() function to execute system curl command +###################################################################### +sub curl_cmd { + my @params = @_; + my $tmpfile; + my $tfh; + my $system_curl = quotemeta(subst_var('@CURL@', 'curl')); + my %curl_codes = ( ## Subset of error codes from https://curl.haxx.se/docs/manpage.html + 2 => "Failed to initialize. (Most likely a bug in ddclient, please open issue at https://github.com/ddclient/ddclient)", + 3 => "URL malformed. The syntax was not correct", + 5 => "Couldn't resolve proxy. The given proxy host could not be resolved.", + 6 => "Couldn't resolve host. The given remote host was not resolved.", + 7 => "Failed to connect to host.", + 22 => "HTTP page not retrieved. The requested url was not found or returned another error.", + 28 => "Operation timeout. The specified time-out period was reached according to the conditions.", + 35 => "SSL connect error. The SSL handshaking failed.", + 47 => "Too many redirects. When following redirects, curl hit the maximum amount.", + 52 => "The server didn't reply anything, which here is considered an error.", + 51 => "The peer's SSL certificate or SSH MD5 fingerprint was not OK.", + 58 => "Problem with the local certificate.", + 60 => "Peer certificate cannot be authenticated with known CA certificates.", + 67 => "The user name, password, or similar was not accepted and curl failed to log in.", + 77 => "Problem with reading the SSL CA cert (path? access rights?).", + 78 => "The resource referenced in the URL does not exist.", + 127 => "You requested network access with curl but $system_curl was not found", + ); + + debug("CURL: %s", $system_curl); + fatal("curl not found") if ($system_curl eq ''); + return '' if (scalar(@params) == 0); ## no parameters provided + + # Hard code to /tmp rather than use system TMPDIR to protect from malicious + # shell instructions in TMPDIR environment variable. All systems should have /tmp. + $tfh = File::Temp->new(DIR => '/tmp', + TEMPLATE => 'ddclient_XXXXXXXXXX'); + $tmpfile = $tfh->filename; + + debug("CURL Tempfile: %s", $tmpfile); + { + local $\ = "\n"; ## Terminate the file, + local $, = "\n"; ## and each parameter, with a newline. + print($tfh @params); + } + close($tfh); + my $reply = qx{ $system_curl --config $tmpfile 2>/dev/null; }; + if ((my $rc = $?>>8) != 0) { + warning("CURL error (%d) %s", $rc, $curl_codes{$rc} // "Unknown return code. Check $system_curl is installed and its manpage."); + } + return $reply; +} + +###################################################################### +## escape_curl_param() makes sure any special characters within a +## curl parameter is properly escaped. +###################################################################### +sub escape_curl_param { + my $str = shift // ''; + + return '' if ($str eq ''); + $str =~ s/"/\\"/g; ## Escape double-quotes + $str =~ s/\\/\\\\/g;## Escape backslashes + $str =~ s/\n/\\n/g; ## Escape newline + $str =~ s/\r/\\r/g; ## Escape carrage return + $str =~ s/\t/\\t/g; ## Escape tabs + $str =~ s/\v/\\v/g; ## Escape vertical whitespace + return $str; +} + +###################################################################### +## fetch_via_curl() is used for geturl() when global curl option set +###################################################################### +sub fetch_via_curl { + my %params = @_; + my $proxy = $params{proxy}; + my $url = $params{url}; + my $login = $params{login}; + my $password = $params{password}; + my $ipversion = ($params{ipversion}) ? int($params{ipversion}) : 0; + my $headers = $params{headers} // ''; + my $method = $params{method} // 'GET'; + my $data = $params{data} // ''; + + my $reply; + my $server; + my $use_ssl = 0; + my $force_ssl = 0; + my $protocol; + my $timeout = opt('timeout'); + my @curlopt = (); + my @header_lines = (); + + ## canonify proxy and url + $force_ssl = 1 if ($url =~ /^https:/); + $proxy =~ s%^https?://%%i if defined($proxy); + $url =~ s%^https?://%%i; + $server = $url; + $server =~ s%[?/].*%%; + $url =~ s%^[^?/]*/?%%; + + $use_ssl = 1 if ($force_ssl || ($globals{'ssl'} && !($params{ignore_ssl_option} // 0))); + + $protocol = ($use_ssl ? "https" : "http"); + + debug("proxy = %s", $proxy // ''); + debug("protocol = %s", $protocol); + debug("server = %s", $server); + debug("url = %s", $url); + debug("ip ver = %s", $ipversion); + + if (!opt('exec')) { + debug("skipped network connection"); + verbose("SENDING:", "%s", "${server}/${url}"); + } else { + push(@curlopt, "silent"); + push(@curlopt, "include"); ## Include HTTP response for compatibility + push(@curlopt, "insecure") if ($use_ssl && !($params{ssl_validate} // 1)); + push(@curlopt, "cacert=\"".escape_curl_param(opt('ssl_ca_file')).'"') if defined(opt('ssl_ca_file')); + push(@curlopt, "capath=\"".escape_curl_param(opt('ssl_ca_dir')).'"') if defined(opt('ssl_ca_dir')); + push(@curlopt, "ipv4") if ($ipversion == 4); + push(@curlopt, "ipv6") if ($ipversion == 6); + push(@curlopt, "user-agent=\"".escape_curl_param("${program}/${version}").'"'); + push(@curlopt, "connect-timeout=$timeout"); + push(@curlopt, "max-time=$timeout"); + push(@curlopt, "request=$method"); + push(@curlopt, "user=\"".escape_curl_param("${login}:${password}").'"') if (defined($login) && defined($password)); + push(@curlopt, "proxy=\"".escape_curl_param("${protocol}://${proxy}").'"') if defined($proxy); + push(@curlopt, "url=\"".escape_curl_param("${protocol}://${server}/${url}").'"'); + @header_lines = split('\n', $headers); + $_ = "header=\"".escape_curl_param($_).'"' foreach (@header_lines); + push(@curlopt, @header_lines); + push(@curlopt, "data=\"".escape_curl_param(${data}).'"') if ($data ne ''); + + # don't include ${url} as that might expose login credentials + $0 = sprintf("%s - curl sending to %s", $program, "${protocol}://${server}"); + verbose("SENDING:", "curl to %s", "${protocol}://${server}"); + + $reply = curl_cmd(@curlopt); + + if (!$reply) { + # don't include ${url} as that might expose login credentials + warning("curl cannot connect to %s://%s using IPv%s",${protocol},${server},$ipversion); + } + } + + ## during testing simulate reading the URL + if (opt('test')) { + my $filename = "$server/$url"; + $filename =~ s|/|%2F|g; + if (opt('exec')) { + $reply = save_file("$savedir/$filename", $reply, 'unique'); + } else { + $reply = load_file("$savedir/$filename"); + } + } + + $reply =~ s/\r//g if defined $reply; + return $reply; +} + ###################################################################### ## get_ip ###################################################################### @@ -2247,7 +2422,11 @@ sub get_ip { $arg = $url; if ($url) { - $reply = geturl(proxy => opt('proxy', $h), url => $url) // ''; + $reply = geturl( + proxy => opt('proxy', $h), + url => $url, + ssl_validate => opt('web-ssl-validate', $h), + ) // ''; } } elsif (($use eq 'cisco')) { @@ -2269,6 +2448,7 @@ sub get_ip { login => opt('fw-login', $h), password => opt('fw-password', $h), ignore_ssl_option => 1, + ssl_validate => opt('fw-ssl-validate', $h), ) // ''; $arg = $url; @@ -2291,6 +2471,7 @@ sub get_ip { login => opt('fw-login', $h), password => opt('fw-password', $h), ignore_ssl_option => 1, + ssl_validate => opt('fw-ssl-validate', $h), ) // ''; $arg = $url; @@ -2310,6 +2491,7 @@ sub get_ip { login => opt('fw-login', $h), password => opt('fw-password', $h), ignore_ssl_option => 1, + ssl_validate => opt('fw-ssl-validate', $h), ) // ''; } } diff --git a/t/geturl_connectivity.pl b/t/geturl_connectivity.pl.in similarity index 79% rename from t/geturl_connectivity.pl rename to t/geturl_connectivity.pl.in index bc0d56b..2e825d0 100644 --- a/t/geturl_connectivity.pl +++ b/t/geturl_connectivity.pl.in @@ -13,6 +13,8 @@ my $ipv6_supported = eval { ); defined($ipv6_socket); }; +my $has_curl = qx{ @CURL@ --version 2>/dev/null; } && $? == 0; + my $http_daemon_supports_ipv6 = eval { require HTTP::Daemon; HTTP::Daemon->VERSION(6.12); @@ -71,23 +73,39 @@ my @test_cases = ( {ssl => 1, server_ipv => '4', client_ipv => '4'}, {ssl => 1, server_ipv => '6', client_ipv => ''}, {ssl => 1, server_ipv => '6', client_ipv => '6'}, + + # Fetch with curl + { curl => 1, server_ipv => '4', client_ipv => '' }, + { curl => 1, server_ipv => '4', client_ipv => '4' }, + { curl => 1, server_ipv => '6', client_ipv => '' }, + { curl => 1, server_ipv => '6', client_ipv => '6' }, + + # Fetch with curl and ssl + { curl => 1, ssl => 1, server_ipv => '4', client_ipv => '' }, + { curl => 1, ssl => 1, server_ipv => '4', client_ipv => '4' }, + { curl => 1, ssl => 1, server_ipv => '6', client_ipv => '' }, + { curl => 1, ssl => 1, server_ipv => '6', client_ipv => '6' }, ); for my $tc (@test_cases) { $tc->{ipv6_opt} //= 0; $tc->{ssl} //= 0; + $tc->{curl} //= 0; SKIP: { skip("IO::Socket::INET6 not available", 1) - if ($tc->{ipv6_opt} || $tc->{client_ipv} eq '6') && !$has_io_socket_inet6; + if ($tc->{ipv6_opt} || $tc->{client_ipv} eq '6') && !$tc->{curl} && !$has_io_socket_inet6; skip("IPv6 not supported on this system", 1) if $tc->{server_ipv} eq '6' && !$ipv6_supported; skip("HTTP::Daemon too old for IPv6 support", 1) if $tc->{server_ipv} eq '6' && !$http_daemon_supports_ipv6; skip("HTTP::Daemon::SSL not available", 1) if $tc->{ssl} && !$has_http_daemon_ssl; + skip("Curl not available on this system", 1) if $tc->{curl} && !$has_curl; my $uri = $httpd{$tc->{server_ipv}}{$tc->{ssl} ? 'https' : 'http'}->endpoint(); - my $name = sprintf("IPv%s client to %s%s", - $tc->{client_ipv} || '*', $uri, $tc->{ipv6_opt} ? ' (-ipv6)' : ''); + my $name = sprintf("IPv%s client to %s%s%s", + $tc->{client_ipv} || '*', $uri, $tc->{ipv6_opt} ? ' (-ipv6)' : '', + $tc->{curl} ? ' (curl)' : ''); $ddclient::globals{'ipv6'} = $tc->{ipv6_opt}; + $ddclient::globals{'curl'} = $tc->{curl}; my $got = ddclient::geturl(url => $uri, ipversion => $tc->{client_ipv}); isnt($got // '', '', $name); } diff --git a/t/geturl_ssl.pl b/t/geturl_ssl.pl index 9b0212f..c070def 100644 --- a/t/geturl_ssl.pl +++ b/t/geturl_ssl.pl @@ -4,6 +4,7 @@ eval { require HTTP::Request; require HTTP::Response; require IO::Socket::IP; + require IO::Socket::SSL; require ddclient::Test::Fake::HTTPD; } or plan(skip_all => $@); SKIP: { eval { require Test::Warnings; } or skip($@, 1); } @@ -57,6 +58,7 @@ my @test_cases = ( want_args => { PeerAddr => 'hostname', PeerPort => '443', + SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER, }, want_req_uri => '/', }, @@ -69,6 +71,7 @@ my @test_cases = ( want_args => { PeerAddr => 'hostname', PeerPort => '443', + SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER, }, want_req_uri => '/', }, @@ -80,6 +83,7 @@ my @test_cases = ( want_args => { PeerAddr => 'hostname', PeerPort => '123', + SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER, }, want_req_uri => '/', }, @@ -92,6 +96,7 @@ my @test_cases = ( want_args => { PeerAddr => 'hostname', PeerPort => '123', + SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER, }, want_req_uri => '/', }, @@ -104,6 +109,7 @@ my @test_cases = ( want_args => { PeerAddr => 'proxy', PeerPort => '443', + SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER, }, want_req_uri => 'http://hostname/', todo => "broken", @@ -132,6 +138,7 @@ my @test_cases = ( want_args => { PeerAddr => 'proxy', PeerPort => '443', + SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER, }, want_req_method => 'CONNECT', want_req_uri => 'hostname:443', @@ -147,6 +154,7 @@ my @test_cases = ( want_args => { PeerAddr => 'proxy', PeerPort => '443', + SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER, }, want_req_method => 'CONNECT', want_req_uri => 'hostname:443', @@ -190,6 +198,7 @@ my @test_cases = ( PeerAddr => 'hostname', PeerPort => '443', SSL_ca_path => '/ca/dir', + SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER, }, want_req_uri => '/', }, @@ -203,6 +212,7 @@ my @test_cases = ( PeerAddr => 'hostname', PeerPort => '443', SSL_ca_file => '/ca/file', + SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER, }, want_req_uri => '/', }, @@ -218,6 +228,7 @@ my @test_cases = ( PeerPort => '443', SSL_ca_file => '/ca/file', SSL_ca_path => '/ca/dir', + SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER, }, want_req_uri => '/', }, From 8106b3025f8aaed6637cc4e3e5c35e1529926135 Mon Sep 17 00:00:00 2001 From: David Kerr Date: Sun, 2 Aug 2020 16:17:52 -0400 Subject: [PATCH 02/16] Add support to use WWW::Curl::Easy if it exists --- ddclient.in | 99 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 77 insertions(+), 22 deletions(-) diff --git a/ddclient.in b/ddclient.in index acbe532..19cecf2 100755 --- a/ddclient.in +++ b/ddclient.in @@ -2336,31 +2336,86 @@ sub fetch_via_curl { debug("skipped network connection"); verbose("SENDING:", "%s", "${server}/${url}"); } else { - push(@curlopt, "silent"); - push(@curlopt, "include"); ## Include HTTP response for compatibility - push(@curlopt, "insecure") if ($use_ssl && !($params{ssl_validate} // 1)); - push(@curlopt, "cacert=\"".escape_curl_param(opt('ssl_ca_file')).'"') if defined(opt('ssl_ca_file')); - push(@curlopt, "capath=\"".escape_curl_param(opt('ssl_ca_dir')).'"') if defined(opt('ssl_ca_dir')); - push(@curlopt, "ipv4") if ($ipversion == 4); - push(@curlopt, "ipv6") if ($ipversion == 6); - push(@curlopt, "user-agent=\"".escape_curl_param("${program}/${version}").'"'); - push(@curlopt, "connect-timeout=$timeout"); - push(@curlopt, "max-time=$timeout"); - push(@curlopt, "request=$method"); - push(@curlopt, "user=\"".escape_curl_param("${login}:${password}").'"') if (defined($login) && defined($password)); - push(@curlopt, "proxy=\"".escape_curl_param("${protocol}://${proxy}").'"') if defined($proxy); - push(@curlopt, "url=\"".escape_curl_param("${protocol}://${server}/${url}").'"'); - @header_lines = split('\n', $headers); - $_ = "header=\"".escape_curl_param($_).'"' foreach (@header_lines); - push(@curlopt, @header_lines); - push(@curlopt, "data=\"".escape_curl_param(${data}).'"') if ($data ne ''); + my $curl_loaded = eval { require WWW::Curl::Easy }; + if ($curl_loaded) { + # System has the WWW::Curl::Easy module so use that + import WWW::Curl::Easy; + my $curl = WWW::Curl::Easy->new; - # don't include ${url} as that might expose login credentials - $0 = sprintf("%s - curl sending to %s", $program, "${protocol}://${server}"); - verbose("SENDING:", "curl to %s", "${protocol}://${server}"); + $curl->setopt(WWW::Curl::Easy->CURLOPT_HEADER, 1); ## Include HTTP response for compatibility + $curl->setopt(WWW::Curl::Easy->CURLOPT_SSL_VERIFYPEER, ($params{ssl_validate} // 1) ? 1 : 0 ); + $curl->setopt(WWW::Curl::Easy->CURLOPT_SSL_VERIFYHOST, ($params{ssl_validate} // 1) ? 1 : 0 ); + $curl->setopt(WWW::Curl::Easy->CURLOPT_CAINFO, opt('ssl_ca_file')) if defined(opt('ssl_ca_file')); + $curl->setopt(WWW::Curl::Easy->CURLOPT_CAPATH, opt('ssl_ca_dir')) if defined(opt('ssl_ca_dir')); + $curl->setopt(WWW::Curl::Easy->CURLOPT_IPRESOLVE, + ($ipversion == 4) ? WWW::Curl::Easy->CURL_IPRESOLVE_V4 : + ($ipversion == 6) ? WWW::Curl::Easy->CURL_IPRESOLVE_V6 : + WWW::Curl::Easy->CURL_IPRESOLVE_WHATEVER); + $curl->setopt(WWW::Curl::Easy->CURLOPT_USERAGENT, "${program}/${version}"); + $curl->setopt(WWW::Curl::Easy->CURLOPT_CONNECTTIMEOUT, $timeout); + $curl->setopt(WWW::Curl::Easy->CURLOPT_TIMEOUT, $timeout); - $reply = curl_cmd(@curlopt); + $curl->setopt(WWW::Curl::Easy->CURLOPT_POST, 1) if ($method eq 'POST'); + $curl->setopt(WWW::Curl::Easy->CURLOPT_PUT, 1) if ($method eq 'PUT'); + $curl->setopt(WWW::Curl::Easy->CURLOPT_CUSTOMREQUEST, $method) if ($method ne 'GET'); ## for PATCH + $curl->setopt(WWW::Curl::Easy->CURLOPT_USERPWD, "${login}:${password}") if (defined($login) && defined($password)); + $curl->setopt(WWW::Curl::Easy->CURLOPT_PROXY, "${protocol}://${proxy}") if defined($proxy); + $curl->setopt(WWW::Curl::Easy->CURLOPT_URL, "${protocol}://${server}/${url}"); + + # Each header line is added individually + @header_lines = split('\n', $headers); + $curl->pushopt(WWW::Curl::Easy->CURLOPT_HTTPHEADER, $_) foreach (@header_lines); + + # Add in the data if any was provided (for POST/PATCH) + if (my $datalen = length($data)) { + $curl->setopt(WWW::Curl::Easy->CURLOPT_POSTFIELDS, ${data}); + $curl->setopt(WWW::Curl::Easy->CURLOPT_POSTFIELDSIZE, $datalen); + } + $curl->setopt(WWW::Curl::Easy->CURLOPT_WRITEDATA,\$reply); + + # don't include ${url} as that might expose login credentials + $0 = sprintf("%s - WWW::Curl::Easy sending to %s", $program, "${protocol}://${server}"); + verbose("SENDING:", "WWW::Curl::Easy to %s", "${protocol}://${server}"); + + my $rc = $curl->perform; + + if ($rc != 0) { + warning("CURL error (%d) %s", $rc, $curl->strerror($rc)); + debug($curl->errbuf); + } + } else { + # System does not have the WWW::Curl::Easy module so attempt with system Curl command + + push(@curlopt, "silent"); + push(@curlopt, "include"); ## Include HTTP response for compatibility + push(@curlopt, "insecure") if ($use_ssl && !($params{ssl_validate} // 1)); + push(@curlopt, "cacert=\"".escape_curl_param(opt('ssl_ca_file')).'"') if defined(opt('ssl_ca_file')); + push(@curlopt, "capath=\"".escape_curl_param(opt('ssl_ca_dir')).'"') if defined(opt('ssl_ca_dir')); + push(@curlopt, "ipv4") if ($ipversion == 4); + push(@curlopt, "ipv6") if ($ipversion == 6); + push(@curlopt, "user-agent=\"".escape_curl_param("${program}/${version}").'"'); + push(@curlopt, "connect-timeout=$timeout"); + push(@curlopt, "max-time=$timeout"); + push(@curlopt, "request=$method"); + push(@curlopt, "user=\"".escape_curl_param("${login}:${password}").'"') if (defined($login) && defined($password)); + push(@curlopt, "proxy=\"".escape_curl_param("${protocol}://${proxy}").'"') if defined($proxy); + push(@curlopt, "url=\"".escape_curl_param("${protocol}://${server}/${url}").'"'); + + # Each header line is added individually + @header_lines = split('\n', $headers); + $_ = "header=\"".escape_curl_param($_).'"' foreach (@header_lines); + push(@curlopt, @header_lines); + + # Add in the data if any was provided (for POST/PATCH) + push(@curlopt, "data=\"".escape_curl_param(${data}).'"') if ($data ne ''); + + # don't include ${url} as that might expose login credentials + $0 = sprintf("%s - Curl system cmd sending to %s", $program, "${protocol}://${server}"); + verbose("SENDING:", "Curl system cmd to %s", "${protocol}://${server}"); + + $reply = curl_cmd(@curlopt); + } if (!$reply) { # don't include ${url} as that might expose login credentials warning("curl cannot connect to %s://%s using IPv%s",${protocol},${server},$ipversion); From cde60432dd90095b98a9e07db7e80f33582ccb0f Mon Sep 17 00:00:00 2001 From: David Kerr Date: Wed, 12 Aug 2020 12:53:59 -0400 Subject: [PATCH 03/16] Remove from debug message in geturl() parameters sent as part of a URL --- ddclient.in | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ddclient.in b/ddclient.in index 19cecf2..71ac6ea 100755 --- a/ddclient.in +++ b/ddclient.in @@ -2084,7 +2084,8 @@ sub fetch_via_socket_io { debug("proxy = %s", $proxy // ''); debug("protocol = %s", $use_ssl ? "https" : "http"); debug("server = %s", $server); - debug("url = %s", $url); + (my $_url = $url) =~ s%\?.*%?%; #redact possible credentials + debug("url = %s", $_url); debug("ip ver = %s", $ipversion); ## determine peer and port to use. @@ -2329,7 +2330,8 @@ sub fetch_via_curl { debug("proxy = %s", $proxy // ''); debug("protocol = %s", $protocol); debug("server = %s", $server); - debug("url = %s", $url); + (my $_url = $url) =~ s%\?.*%?%; #redact possible credentials + debug("url = %s", $_url); debug("ip ver = %s", $ipversion); if (!opt('exec')) { From 3a73e5e6b48fc601202749c862514a8ce6f2e4ac Mon Sep 17 00:00:00 2001 From: David Kerr Date: Fri, 21 Aug 2020 16:59:07 -0400 Subject: [PATCH 04/16] header_ok() function must accept HTTP versions other than just '1' --- ddclient.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddclient.in b/ddclient.in index 71ac6ea..f9c6a37 100755 --- a/ddclient.in +++ b/ddclient.in @@ -2913,7 +2913,7 @@ sub header_ok { my ($host, $line) = @_; my $ok = 0; - if ($line =~ m%^s*HTTP/1.*\s+(\d+)%i) { + if ($line =~ m%^s*HTTP/.*\s+(\d+)%i) { my $result = $1; if ($result =~ m/^2\d\d$/) { From 4c79c6b607ad691cbab7f9a24f4865034f1ccad9 Mon Sep 17 00:00:00 2001 From: David Kerr Date: Fri, 21 Aug 2020 17:01:13 -0400 Subject: [PATCH 05/16] must not escape the 'data' field sent to servers. --- ddclient.in | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ddclient.in b/ddclient.in index f9c6a37..677ce60 100755 --- a/ddclient.in +++ b/ddclient.in @@ -2379,6 +2379,8 @@ sub fetch_via_curl { # don't include ${url} as that might expose login credentials $0 = sprintf("%s - WWW::Curl::Easy sending to %s", $program, "${protocol}://${server}"); verbose("SENDING:", "WWW::Curl::Easy to %s", "${protocol}://${server}"); + verbose("SENDING:", "%s", $headers) if ($headers); + verbose("SENDING:", "%s", $data) if ($data); my $rc = $curl->perform; @@ -2410,14 +2412,16 @@ sub fetch_via_curl { push(@curlopt, @header_lines); # Add in the data if any was provided (for POST/PATCH) - push(@curlopt, "data=\"".escape_curl_param(${data}).'"') if ($data ne ''); + push(@curlopt, "data=${data}") if ($data); # don't include ${url} as that might expose login credentials $0 = sprintf("%s - Curl system cmd sending to %s", $program, "${protocol}://${server}"); verbose("SENDING:", "Curl system cmd to %s", "${protocol}://${server}"); + verbose("SENDING:", "%s", $_) foreach (@curlopt); $reply = curl_cmd(@curlopt); } + verbose("RECEIVE:", "%s", $reply // ""); if (!$reply) { # don't include ${url} as that might expose login credentials warning("curl cannot connect to %s://%s using IPv%s",${protocol},${server},$ipversion); From 6ae64e6cfbee65a6464dfb4e39835a33d6361fb8 Mon Sep 17 00:00:00 2001 From: David Kerr Date: Fri, 21 Aug 2020 17:22:04 -0400 Subject: [PATCH 06/16] bug fix... segfault when using WWW::Curl::Easy->pushopt() replace with setopt() --- ddclient.in | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ddclient.in b/ddclient.in index 677ce60..be14ab4 100755 --- a/ddclient.in +++ b/ddclient.in @@ -2365,10 +2365,11 @@ sub fetch_via_curl { $curl->setopt(WWW::Curl::Easy->CURLOPT_PROXY, "${protocol}://${proxy}") if defined($proxy); $curl->setopt(WWW::Curl::Easy->CURLOPT_URL, "${protocol}://${server}/${url}"); - # Each header line is added individually - @header_lines = split('\n', $headers); - $curl->pushopt(WWW::Curl::Easy->CURLOPT_HTTPHEADER, $_) foreach (@header_lines); - + # Add header lines if any was provided + if ($headers) { + @header_lines = split('\n', $headers); + $curl->setopt(WWW::Curl::Easy->CURLOPT_HTTPHEADER, \@header_lines); + } # Add in the data if any was provided (for POST/PATCH) if (my $datalen = length($data)) { $curl->setopt(WWW::Curl::Easy->CURLOPT_POSTFIELDS, ${data}); @@ -2390,7 +2391,6 @@ sub fetch_via_curl { } } else { # System does not have the WWW::Curl::Easy module so attempt with system Curl command - push(@curlopt, "silent"); push(@curlopt, "include"); ## Include HTTP response for compatibility push(@curlopt, "insecure") if ($use_ssl && !($params{ssl_validate} // 1)); From d1d7548e090ea3d52222557d1220757d62200c37 Mon Sep 17 00:00:00 2001 From: David Kerr Date: Sat, 22 Aug 2020 21:32:27 -0400 Subject: [PATCH 07/16] So the bug was actually in the escape_curl_param function, not that the data should not be escaped --- ddclient.in | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ddclient.in b/ddclient.in index be14ab4..4a8409b 100755 --- a/ddclient.in +++ b/ddclient.in @@ -2283,8 +2283,8 @@ sub escape_curl_param { my $str = shift // ''; return '' if ($str eq ''); - $str =~ s/"/\\"/g; ## Escape double-quotes $str =~ s/\\/\\\\/g;## Escape backslashes + $str =~ s/"/\\"/g; ## Escape double-quotes $str =~ s/\n/\\n/g; ## Escape newline $str =~ s/\r/\\r/g; ## Escape carrage return $str =~ s/\t/\\t/g; ## Escape tabs @@ -2412,7 +2412,7 @@ sub fetch_via_curl { push(@curlopt, @header_lines); # Add in the data if any was provided (for POST/PATCH) - push(@curlopt, "data=${data}") if ($data); + push(@curlopt, "data=\"".escape_curl_param(${data}).'"') if ($data); # don't include ${url} as that might expose login credentials $0 = sprintf("%s - Curl system cmd sending to %s", $program, "${protocol}://${server}"); From 213cf6ad098a97d713cdaaecb5d2967987abe8b0 Mon Sep 17 00:00:00 2001 From: David Kerr Date: Sun, 2 Aug 2020 22:30:00 -0400 Subject: [PATCH 08/16] Add get_ip_from_interface function --- Makefile.am | 1 + ddclient.in | 121 +++++++++++++- t/get_ip_from_if.pl | 29 ++++ t/is-and-extract-ipv6-global.pl | 6 +- t/is-and-extract-ipv6.pl | 14 +- t/lib/ddclient/t.pm | 277 +++++++++++++++++++++++++++++--- 6 files changed, 412 insertions(+), 36 deletions(-) create mode 100644 t/get_ip_from_if.pl diff --git a/Makefile.am b/Makefile.am index 5e5d633..ce01304 100644 --- a/Makefile.am +++ b/Makefile.am @@ -71,6 +71,7 @@ AM_PL_LOG_FLAGS = -Mstrict -w \ -I'$(abs_top_srcdir)'/t/lib \ -MDevel::Autoflush handwritten_tests = \ + t/get_ip_from_if.pl \ t/geturl_ssl.pl \ t/is-and-extract-ipv4.pl \ t/is-and-extract-ipv6.pl \ diff --git a/ddclient.in b/ddclient.in index 4a8409b..25c1bd1 100755 --- a/ddclient.in +++ b/ddclient.in @@ -2461,9 +2461,7 @@ sub get_ip { $arg = 'ip'; } elsif ($use eq 'if') { - $reply = `command -v ip >/dev/null && ip address show dev $arg`; - $reply = `command -v ifconfig >/dev/null && ifconfig $arg` if $?; - $reply = '' if $?; + $ip = get_ip_from_interface($arg); } elsif ($use eq 'cmd') { if ($arg) { @@ -2696,6 +2694,123 @@ sub extract_ipv6_global { return $ip; } +###################################################################### +## Regex that matches an IPv6 address that is unique local (ULA). +## Accepts embedded leading zeros. +###################################################################### +my $regex_ipv6_ula = qr{ + (?= # Address starts with + f[cd][0-9a-f]{2}: # fc00::/7 RFC4193 ULA + ) + $regex_ipv6 # And is a valid IPv6 address +}xi; + +###################################################################### +## get_ip_from_interface() finds an IPv4 or IPv6 address from a network +## interface. Defaults to IPv4 unless '6' passed as 2nd parameter. +###################################################################### +sub get_ip_from_interface { + my $interface = shift; + my $ipver = int(shift // 4); ## Defaults to IPv4 if not specified + my $scope = lc(shift // "gua"); ## "gua" or "ula" + my $reply = shift // ''; ## Pass in data for unit testing purposes only + my $MacOS = shift // 0; ## For testing can set to 1 if input data is MacOS/FreeBSD format + my $count = 0; + my $cmd = "test"; + + if (($ipver != 4) && ($ipver != 6)) { + warning("get_ip_from_interface() invalid IP version: %s", $ipver); + return undef; + } + + if ($reply eq '') { ## skip if test data passed in. + return undef if !defined($interface); + } + + if ($ipver == 4) { + if (!$reply) { ## skip if test data passed in. + ## Try ip first, then ifconfig. + $cmd = "ip -4 -o addr show dev $interface scope global"; $reply = qx{$cmd 2>/dev/null}; + if ($?) { $cmd = "ifconfig $interface"; $reply = qx{$cmd 2>/dev/null}; } + if ($?) { $cmd = "missing ip or ifconfig command"; + failed("Unable to obtain information for '%s' -- %s", $interface, $cmd); + } + } + debug("Reply from '%s' :\n------\n%s------", $cmd, $reply); + + ## IPv4 is simple, we just need to find the first IPv4 address returned in the list. + my @reply = split(/\n/, $reply); + @reply = grep(/\binet\b/, @reply); # Select only IPv4 entries + return extract_ipv4($reply[0]); + } + + ## From this point on we only looking for IPv6 address. + if (($scope ne "gua") && ($scope ne "ula")) { + warning("get_ip_from_interface() invalid IPv6 scope: %s, using type GUA", $scope); + $scope = "gua"; + } + + $cmd = "test data"; + if (!$reply) { ## skip if test data passed in. + ## Try ip first, then ifconfig with -L for MacOS/FreeBSD then finally ifconfig for everything else + $cmd = "ip -6 -o addr show dev $interface scope global"; $reply = qx{$cmd 2>/dev/null}; # Linux + if ($?) { $cmd = "ifconfig -L $interface"; $MacOS = 1; $reply = qx{$cmd 2>/dev/null}; } # MacOS/FreeBSD + if ($?) { $cmd = "ifconfig $interface"; $reply = qx{$cmd 2>/dev/null}; } # Anything without iproute2 or -L + if ($?) { $cmd = "missing ip or ifconfig command"; + failed("Unable to obtain information for '%s' -- %s", $interface, $cmd); + } + } + debug("Reply from '%s' :\n------\n%s------", $cmd, $reply); + + ## IPv6 is more complex than IPv4. Start by filtering on only "inet6" addresses + ## Then remove deprecated or temporary addresses and finally seleect on global or local addresses + my @reply = split(/\n/, $reply); + @reply = grep(/\binet6\b/, @reply); # Select only IPv6 entries + @reply = grep(!/\bdeprecated\b|\btemporary\b/, @reply); # Remove deprecated and temporary + @reply = ($scope eq "gua") ? grep(/$regex_ipv6_global/, @reply) # Select only global addresses + : grep(/$regex_ipv6_ula/, @reply); # or only ULA addresses + debug("Raw IPv6 after filtering for %s addresses %s: (%s)\r\n%s", uc($scope), $interface, scalar(@reply), join("\n", @reply)); + + ## If we filter down to zero or one result then we are done... + return undef if (($count = scalar(@reply)) == 0); + return extract_ipv6($reply[0]) if ($count == 1); + + ## If there are more than one we need to select the "best". + ## First choice would be a static address. + my @static = ($MacOS == 1) ? grep(!/^.*\bvltime\b.*$/i, @reply) # MacOS/FreeBSD, no 'vltime' + : grep(/^.*\bvalid_lft.\bforever\b.*$/i, @reply); # Everything else 'forever' life + $count = scalar(@static); + debug("Possible Static IP addresses %s: (%s)\r\n%s", $interface, $count, join("\n", @static)); + + ## If only one result then we are done. If there are more than one static addresses + ## then we will replace our original list with the list of statics and sort on them. + ## If zero static addresses we fall through with our original list. + return extract_ipv6($static[0]) if ($count == 1); + @reply = @static if ($count > 1); + + ## Sort what we have by the prefix length, IP address "length" and finally valid life. + my @sorted = sort { + ## We give preference to IP addressess with the longest prefix... so we prefer a /128 over a /64 + ## this is a decimal (\d+) either after the word "prefixlen" or after a forward slash. + (($b =~ /(?:\bprefixlen\b\s*|\/)(\d+)/i)[0] // 0) <=> (($a =~ /(?:\bprefixlen\b\s*|\/)(\d+)/i)[0] // 0) + + ## If there are multiple the same then we prefer "shorter" IP addresses in the + ## theory that a shorter address is more likely assigned by DHCPv6 than SLAAC. + ## E.g. 2001:db8:4341:0781::8214/64 is preferable to 2001:db8:4341:0781:34a6:c329:c52e:8ba6/64 + ## So we count the number () of groups of [0-9a-f] blocks in the IP address. + || (()= (extract_ipv6($a) // '') =~ /[0-9A-F]+/gi) <=> (()= (extract_ipv6($b) // '') =~ /[0-9A-F]+/gi) + + ## Finally we check remaining valid lifetime and prefer longer remaining life. + ## This is a desimal (\d+) after the word "valid_lft" or "vltime". Only available + ## from iproute2 or MacOS/FreeBSD version of ifconfig (-L parameter). + || (($b =~ /(?:\bvalid_lft\b\s*|\bvltime\b\s*)(\d+)/i)[0] // 0) <=> (($a =~ /(?:\bvalid_lft\b\s*|\bvltime\b\s*)(\d+)/i)[0] // 0) + } @reply; + debug("Sorted list of IP addresss for %s: (%s)\r\n%s", $interface, scalar(@sorted), join("\n", @sorted)); + + ## Whatever sorted to the top is the best choice for IPv6 address + return extract_ipv6($sorted[0]); +} + ###################################################################### ## group_hosts_by ###################################################################### diff --git a/t/get_ip_from_if.pl b/t/get_ip_from_if.pl new file mode 100644 index 0000000..d4a0c4b --- /dev/null +++ b/t/get_ip_from_if.pl @@ -0,0 +1,29 @@ +use Test::More; +use ddclient::t; +SKIP: { eval { require Test::Warnings; } or skip($@, 1); } +eval { require 'ddclient'; } or BAIL_OUT($@); + +# To aid in debugging, uncomment the following lines. (They are normally left commented to avoid +# accidentally interfering with the Test Anything Protocol messages written by Test::More.) +#STDOUT->autoflush(1); +#$ddclient::globals{'debug'} = 1; + +subtest "get_ip_from_interface tests" => sub { + for my $sample (@ddclient::t::interface_samples) { + # interface name is undef as we are passing in test data + if (defined($sample->{want_ipv4_from_if})) { + my $ip = ddclient::get_ip_from_interface(undef, 4, undef, $sample->{text}, $sample->{MacOS}); + is($ip, $sample->{want_ipv4_from_if}, $sample->{name}); + } + if (defined($sample->{want_ipv6gua_from_if})) { + my $ip = ddclient::get_ip_from_interface(undef, 6, 'gua', $sample->{text}, $sample->{MacOS}); + is($ip, $sample->{want_ipv6gua_from_if}, $sample->{name}); + } + if (defined($sample->{want_ipv6ula_from_if})) { + my $ip = ddclient::get_ip_from_interface(undef, 6, 'ula', $sample->{text}, $sample->{MacOS}); + is($ip, $sample->{want_ipv6ula_from_if}, $sample->{name}); + } + } +}; + +done_testing(); diff --git a/t/is-and-extract-ipv6-global.pl b/t/is-and-extract-ipv6-global.pl index de7a991..b0e660e 100644 --- a/t/is-and-extract-ipv6-global.pl +++ b/t/is-and-extract-ipv6-global.pl @@ -56,8 +56,10 @@ subtest "extract_ipv6_global()" => sub { subtest "interface config samples" => sub { for my $sample (@ddclient::t::interface_samples) { - my $got = ddclient::extract_ipv6_global($sample->{text}); - is($got, $sample->{want_extract_ipv6_global}, $sample->{name}); + if (defined($sample->{want_extract_ipv6_global})) { + my $got = ddclient::extract_ipv6_global($sample->{text}); + is($got, $sample->{want_extract_ipv6_global}, $sample->{name}); + } } }; diff --git a/t/is-and-extract-ipv6.pl b/t/is-and-extract-ipv6.pl index a7d97b9..7362be6 100644 --- a/t/is-and-extract-ipv6.pl +++ b/t/is-and-extract-ipv6.pl @@ -422,13 +422,17 @@ subtest "extract_ipv6() of valid addr with adjacent non-word char" => sub { subtest "interface config samples" => sub { for my $sample (@ddclient::t::interface_samples) { - subtest $sample->{name} => sub { - my $ip = ddclient::extract_ipv6($sample->{text}); - ok(ddclient::is_ipv6($ip), "extract_ipv6() returns an IPv6 address"); + if (defined($sample->{want_extract_ipv6_global})) { + subtest $sample->{name} => sub { + my $ip = ddclient::extract_ipv6($sample->{text}); + ok(ddclient::is_ipv6($ip), "extract_ipv6() returns an IPv6 address"); + }; foreach my $line (split(/\n/, $sample->{text})) { my $ip = ddclient::extract_ipv6($line); - ok(ddclient::is_ipv6($ip), - sprintf("extract_ipv6(%s) returns an IPv6 address", perlstring($line))); + if ($ip) { ## Test cases may have lines that do not contain IPv6 address. + ok(ddclient::is_ipv6($ip), + sprintf("extract_ipv6(%s) returns an IPv6 address", perlstring($line))); + } } } } diff --git a/t/lib/ddclient/t.pm b/t/lib/ddclient/t.pm index 2468b73..2899e4a 100644 --- a/t/lib/ddclient/t.pm +++ b/t/lib/ddclient/t.pm @@ -3,54 +3,62 @@ require v5.10.1; use strict; use warnings; + +###################################################################### +## Outputs from ip addr and ifconfig commands to find IP address from IF name +## Samples from Ubuntu 20.04, RHEL8, Buildroot, Busybox, MacOS 10.15, FreeBSD +## NOTE: Any tabs/whitespace at start or end of lines are intentional to match real life data. +###################################################################### our @interface_samples = ( - # Sample output from: - # ip -6 -o addr show dev scope global # This seems to be consistent accross platforms. The last line is from Ubuntu of a static # assigned IPv6. { name => 'ip -6 -o addr show dev scope global', text => <<'EOF', 2: ens160 inet6 fdb6:1d86:d9bd:1::8214/128 scope global dynamic noprefixroute \ valid_lft 63197sec preferred_lft 63197sec -2: ens160 inet6 2001:DB8:4341:0781::8214/128 scope global dynamic noprefixroute \ valid_lft 63197sec preferred_lft 63197sec -2: ens160 inet6 2001:DB8:4341:0781:89b9:4b1c:186c:a0c7/64 scope global temporary dynamic \ valid_lft 85954sec preferred_lft 21767sec +2: ens160 inet6 2001:db8:4341:0781::8214/128 scope global dynamic noprefixroute \ valid_lft 63197sec preferred_lft 63197sec +2: ens160 inet6 2001:db8:4341:0781:89b9:4b1c:186c:a0c7/64 scope global temporary dynamic \ valid_lft 85954sec preferred_lft 21767sec 2: ens160 inet6 fdb6:1d86:d9bd:1:89b9:4b1c:186c:a0c7/64 scope global temporary dynamic \ valid_lft 85954sec preferred_lft 21767sec 2: ens160 inet6 fdb6:1d86:d9bd:1:34a6:c329:c52e:8ba6/64 scope global temporary deprecated dynamic \ valid_lft 85954sec preferred_lft 0sec 2: ens160 inet6 fdb6:1d86:d9bd:1:b417:fe35:166b:4816/64 scope global dynamic mngtmpaddr noprefixroute \ valid_lft 85954sec preferred_lft 85954sec -2: ens160 inet6 2001:DB8:4341:0781:34a6:c329:c52e:8ba6/64 scope global temporary deprecated dynamic \ valid_lft 85954sec preferred_lft 0sec -2: ens160 inet6 2001:DB8:4341:0781:f911:a224:7e69:d22/64 scope global dynamic mngtmpaddr noprefixroute \ valid_lft 85954sec preferred_lft 85954sec -2: ens160 inet6 2001:DB8:4341:0781::100/128 scope global noprefixroute \ valid_lft forever preferred_lft forever +2: ens160 inet6 2001:db8:4341:0781:34a6:c329:c52e:8ba6/64 scope global temporary deprecated dynamic \ valid_lft 85954sec preferred_lft 0sec +2: ens160 inet6 2001:db8:4341:0781:f911:a224:7e69:d22/64 scope global dynamic mngtmpaddr noprefixroute \ valid_lft 85954sec preferred_lft 85954sec +2: ens160 inet6 2001:db8:4341:0781::100/128 scope global noprefixroute \ valid_lft forever preferred_lft forever EOF - want_extract_ipv6_global => '2001:DB8:4341:781::8214', + want_extract_ipv6_global => '2001:db8:4341:781::8214', + want_ipv6gua_from_if => "2001:db8:4341:781::100", + want_ipv6ula_from_if => "fdb6:1d86:d9bd:1::8214", }, - # Sample output from MacOS: - # ifconfig | grep -w "inet6" - # (Yes, there is a tab at start of each line.) The last two lines are with a manually + # (Yes, there is a tab at start of each line.) The last lines is with a manually # configured static GUA. { name => 'MacOS: ifconfig | grep -w inet6', + MacOS => 1, text => <<'EOF', inet6 fe80::1419:abd0:5943:8bbb%en0 prefixlen 64 secured scopeid 0xa inet6 fdb6:1d86:d9bd:1:142c:8e9e:de48:843e prefixlen 64 autoconf secured inet6 fdb6:1d86:d9bd:1:7447:cf67:edbd:cea4 prefixlen 64 autoconf temporary inet6 fdb6:1d86:d9bd:1::c5b3 prefixlen 64 dynamic - inet6 2001:DB8:4341:0781:141d:66b9:2ba1:b67d prefixlen 64 autoconf secured - inet6 2001:DB8:4341:0781:64e1:b68f:e8af:5d6e prefixlen 64 autoconf temporary - inet6 fe80::1419:abd0:5943:8bbb%en0 prefixlen 64 secured scopeid 0xa - inet6 2001:DB8:4341:0781::101 prefixlen 64 + inet6 2001:db8:4341:0781:141d:66b9:2ba1:b67d prefixlen 64 autoconf secured + inet6 2001:db8:4341:0781:64e1:b68f:e8af:5d6e prefixlen 64 autoconf temporary + inet6 2001:db8:4341:0781::101 prefixlen 64 EOF - want_extract_ipv6_global => '2001:DB8:4341:781:141d:66b9:2ba1:b67d', + want_extract_ipv6_global => '2001:db8:4341:781:141d:66b9:2ba1:b67d', + want_ipv6gua_from_if => "2001:db8:4341:781::101", + want_ipv6ula_from_if => "fdb6:1d86:d9bd:1::c5b3", }, { name => 'RHEL: ifconfig | grep -w inet6', text => <<'EOF', - inet6 2001:DB8:4341:0781::dc14 prefixlen 128 scopeid 0x0 + inet6 2001:db8:4341:0781::dc14 prefixlen 128 scopeid 0x0 inet6 fe80::cd48:4a58:3b0f:4d30 prefixlen 64 scopeid 0x20 - inet6 2001:DB8:4341:0781:e720:3aec:a936:36d4 prefixlen 64 scopeid 0x0 + inet6 2001:db8:4341:0781:e720:3aec:a936:36d4 prefixlen 64 scopeid 0x0 inet6 fdb6:1d86:d9bd:1:9c16:8cbf:ae33:f1cc prefixlen 64 scopeid 0x0 inet6 fdb6:1d86:d9bd:1::dc14 prefixlen 128 scopeid 0x0 EOF - want_extract_ipv6_global => '2001:DB8:4341:781::dc14', + want_extract_ipv6_global => '2001:db8:4341:781::dc14', + want_ipv6gua_from_if => "2001:db8:4341:781::dc14", + want_ipv6ula_from_if => "fdb6:1d86:d9bd:1::dc14", }, { name => 'Ubuntu: ifconfig | grep -w inet6', @@ -60,19 +68,236 @@ EOF inet6 fdb6:1d86:d9bd:1::8214 prefixlen 128 scopeid 0x0 inet6 fdb6:1d86:d9bd:1:b417:fe35:166b:4816 prefixlen 64 scopeid 0x0 inet6 fe80::5b31:fc63:d353:da68 prefixlen 64 scopeid 0x20 - inet6 2001:DB8:4341:0781::8214 prefixlen 128 scopeid 0x0 - inet6 2001:DB8:4341:0781:34a6:c329:c52e:8ba6 prefixlen 64 scopeid 0x0 - inet6 2001:DB8:4341:0781:89b9:4b1c:186c:a0c7 prefixlen 64 scopeid 0x0 - inet6 2001:DB8:4341:0781:f911:a224:7e69:d22 prefixlen 64 scopeid 0x0 + inet6 2001:db8:4341:0781::8214 prefixlen 128 scopeid 0x0 + inet6 2001:db8:4341:0781:34a6:c329:c52e:8ba6 prefixlen 64 scopeid 0x0 + inet6 2001:db8:4341:0781:89b9:4b1c:186c:a0c7 prefixlen 64 scopeid 0x0 + inet6 2001:db8:4341:0781:f911:a224:7e69:d22 prefixlen 64 scopeid 0x0 EOF - want_extract_ipv6_global => '2001:DB8:4341:781::8214', + want_extract_ipv6_global => '2001:db8:4341:781::8214', + want_ipv6gua_from_if => "2001:db8:4341:781::8214", + want_ipv6ula_from_if => "fdb6:1d86:d9bd:1::8214", }, { name => 'Busybox: ifconfig | grep -w inet6', text => <<'EOF', inet6 addr: fe80::4362:31ff:fe08:61b4/64 Scope:Link - inet6 addr: 2001:DB8:4341:0781:ed44:eb63:b070:212f/128 Scope:Global + inet6 addr: 2001:db8:4341:781:ed44:eb63:b070:212f/128 Scope:Global EOF - want_extract_ipv6_global => '2001:DB8:4341:781:ed44:eb63:b070:212f', + want_extract_ipv6_global => '2001:db8:4341:781:ed44:eb63:b070:212f', + want_ipv6gua_from_if => "2001:db8:4341:781:ed44:eb63:b070:212f", + }, + { name => "ip -4 -o addr show dev ens33 scope global (most linux IPv4)", + text => < "198.51.100.33", + }, + { name => "ip -6 -o addr show dev ens33 scope global (most linux)", + text => < "2001:db8:450a:e723:adee:be82:7fba:ffb2", + want_ipv6gua_from_if => "2001:db8:450a:e723::21", + want_ipv6ula_from_if => "fdb6:1d86:d9bd:3::21", + }, + { name => "ip -6 -o addr show dev ens33 scope global (most linux static IPv6)", + text => < "2001:db8:450a:e723::101", + want_ipv6gua_from_if => "2001:db8:450a:e723::101", + }, + { name => "ifconfig ens33 (most linux autoconf IPv6 and DHCPv6)", + text => < mtu 1500 + inet 198.51.100.33 netmask 255.255.255.0 broadcast 198.51.100.255 + inet6 fdb6:1d86:d9bd:3::21 prefixlen 128 scopeid 0x0 + inet6 fe80::32c0:b270:245b:d3b4 prefixlen 64 scopeid 0x20 + inet6 fdb6:1d86:d9bd:3:a1fd:1ed9:6211:4268 prefixlen 64 scopeid 0x0 + inet6 2001:db8:450a:e723:adee:be82:7fba:ffb2 prefixlen 64 scopeid 0x0 + inet6 2001:db8:450a:e723::21 prefixlen 128 scopeid 0x0 + inet6 fdb6:1d86:d9bd:3:adee:be82:7fba:ffb2 prefixlen 64 scopeid 0x0 + inet6 2001:db8:450a:e723:dbc5:1c4e:9e9b:97a2 prefixlen 64 scopeid 0x0 + ether 00:00:00:da:24:b1 txqueuelen 1000 (Ethernet) + RX packets 3782541 bytes 556082941 (556.0 MB) + RX errors 0 dropped 513 overruns 0 frame 0 + TX packets 33294 bytes 6838768 (6.8 MB) + TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 +EOF + want_extract_ipv6_global => "2001:db8:450a:e723:adee:be82:7fba:ffb2", + want_ipv6gua_from_if => "2001:db8:450a:e723::21", + want_ipv6ula_from_if => "fdb6:1d86:d9bd:3::21", + want_ipv4_from_if => "198.51.100.33", + }, + { name => "ifconfig ens33 (most linux DHCPv6)", + text => < mtu 1500 + inet 198.51.100.33 netmask 255.255.255.0 broadcast 198.51.100.255 + inet6 fdb6:1d86:d9bd:3::21 prefixlen 128 scopeid 0x0 + inet6 fe80::32c0:b270:245b:d3b4 prefixlen 64 scopeid 0x20 + inet6 2001:db8:450a:e723::21 prefixlen 128 scopeid 0x0 + ether 00:00:00:da:24:b1 txqueuelen 1000 (Ethernet) + RX packets 3781554 bytes 555602847 (555.6 MB) + RX errors 0 dropped 513 overruns 0 frame 0 + TX packets 32493 bytes 6706131 (6.7 MB) + TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 +EOF + want_extract_ipv6_global => "2001:db8:450a:e723::21", + want_ipv6gua_from_if => "2001:db8:450a:e723::21", + want_ipv6ula_from_if => "fdb6:1d86:d9bd:3::21", + want_ipv4_from_if => "198.51.100.33", + }, + { name => "ifconfig ens33 (most linux static IPv6)", + text => < mtu 1500 + inet 198.51.100.33 netmask 255.255.255.0 broadcast 198.51.100.255 + inet6 fe80::32c0:b270:245b:d3b4 prefixlen 64 scopeid 0x20 + inet6 2001:db8:450a:e723::101 prefixlen 64 scopeid 0x0 + ether 00:00:00:da:24:b1 txqueuelen 1000 (Ethernet) + RX packets 3780219 bytes 554967876 (554.9 MB) + RX errors 0 dropped 513 overruns 0 frame 0 + TX packets 31556 bytes 6552122 (6.5 MB) + TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 +EOF + want_extract_ipv6_global => "2001:db8:450a:e723::101", + want_ipv6gua_from_if => "2001:db8:450a:e723::101", + want_ipv4_from_if => "198.51.100.33", + }, + { name => "ifconfig en0 (MacOS IPv4)", + text => < mtu 9000 + options=50b + ether 00:00:00:90:32:8f + inet6 fe80::85b:d150:cdd9:3198%en0 prefixlen 64 secured scopeid 0x4 + inet6 2001:db8:450a:e723:1c99:99e2:21d0:79e6 prefixlen 64 autoconf secured + inet6 2001:db8:450a:e723:808d:d894:e4db:157e prefixlen 64 deprecated autoconf temporary + inet6 fdb6:1d86:d9bd:3:837:e1c7:4895:269e prefixlen 64 autoconf secured + inet6 fdb6:1d86:d9bd:3:a0b3:aa4d:9e76:e1ab prefixlen 64 deprecated autoconf temporary + inet 198.51.100.5 netmask 0xffffff00 broadcast 198.51.100.255 + inet6 2001:db8:450a:e723:2474:39fd:f5c0:6845 prefixlen 64 autoconf temporary + inet6 fdb6:1d86:d9bd:3:2474:39fd:f5c0:6845 prefixlen 64 autoconf temporary + inet6 fdb6:1d86:d9bd:3::8076 prefixlen 64 dynamic + nd6 options=201 + media: 1000baseT + status: active +EOF + want_extract_ipv6_global => "2001:db8:450a:e723:1c99:99e2:21d0:79e6", + want_ipv6gua_from_if => "2001:db8:450a:e723:1c99:99e2:21d0:79e6", + want_ipv6ula_from_if => "fdb6:1d86:d9bd:3::8076", + want_ipv4_from_if => "198.51.100.5", + }, + { name => "ifconfig em0 (FreeBSD IPv4)", + text => < metric 0 mtu 1500 + options=81009b + ether 00:00:00:9f:c5:32 + inet6 fe80::20c:29ff:fe9f:c532%em0 prefixlen 64 scopeid 0x1 + inet6 2001:db8:450a:e723:20c:29ff:fe9f:c532 prefixlen 64 autoconf + inet6 fdb6:1d86:d9bd:3:20c:29ff:fe9f:c532 prefixlen 64 autoconf + inet 198.51.100.207 netmask 0xffffff00 broadcast 198.51.100.255 + media: Ethernet autoselect (1000baseT ) + status: active + nd6 options=23 +EOF + want_extract_ipv6_global => "2001:db8:450a:e723:20c:29ff:fe9f:c532", + want_ipv6gua_from_if => "2001:db8:450a:e723:20c:29ff:fe9f:c532", + want_ipv6ula_from_if => "fdb6:1d86:d9bd:3:20c:29ff:fe9f:c532", + want_ipv4_from_if => "198.51.100.207", + }, + { name => "ifconfig -L en0 (MacOS autoconf IPv6)", + MacOS => 1, + text => < mtu 9000 + options=50b + ether 00:00:00:90:32:8f + inet6 fe80::85b:d150:cdd9:3198%en0 prefixlen 64 secured scopeid 0x4 + inet6 2001:db8:450a:e723:1c99:99e2:21d0:79e6 prefixlen 64 autoconf secured pltime 86205 vltime 86205 + inet6 2001:db8:450a:e723:808d:d894:e4db:157e prefixlen 64 deprecated autoconf temporary pltime 0 vltime 86205 + inet6 fdb6:1d86:d9bd:3:837:e1c7:4895:269e prefixlen 64 autoconf secured pltime 86205 vltime 86205 + inet6 fdb6:1d86:d9bd:3:a0b3:aa4d:9e76:e1ab prefixlen 64 deprecated autoconf temporary pltime 0 vltime 86205 + inet 198.51.100.5 netmask 0xffffff00 broadcast 198.51.100.255 + inet6 2001:db8:450a:e723:2474:39fd:f5c0:6845 prefixlen 64 autoconf temporary pltime 76882 vltime 86205 + inet6 fdb6:1d86:d9bd:3:2474:39fd:f5c0:6845 prefixlen 64 autoconf temporary pltime 76882 vltime 86205 + inet6 fdb6:1d86:d9bd:3::8076 prefixlen 64 dynamic pltime 78010 vltime 78010 + nd6 options=201 + media: 1000baseT + status: active +EOF + want_extract_ipv6_global => "2001:db8:450a:e723:1c99:99e2:21d0:79e6", + want_ipv6gua_from_if => "2001:db8:450a:e723:1c99:99e2:21d0:79e6", + want_ipv6ula_from_if => "fdb6:1d86:d9bd:3::8076", + want_ipv4_from_if => "198.51.100.5", + }, + { name => "ifconfig -L en0 (MacOS static IPv6)", + MacOS => 1, + text => < mtu 1500 + options=400 + ether 00:00:00:42:96:eb + inet 198.51.100.199 netmask 0xffffff00 broadcast 198.51.100.255 + inet6 fe80::1445:78b9:1d5c:11eb%en1 prefixlen 64 secured scopeid 0x5 + inet6 2001:db8:450a:e723::100 prefixlen 64 + nd6 options=201 + media: autoselect + status: active +EOF + want_extract_ipv6_global => "2001:db8:450a:e723::100", + want_ipv6gua_from_if => "2001:db8:450a:e723::100", + want_ipv4_from_if => "198.51.100.199", + }, + { name => "ifconfig -L em0 (FreeBSD autoconf IPv6)", + MacOS => 1, + text => < metric 0 mtu 1500 + options=81009b + ether 00:00:00:9f:c5:32 + inet6 fe80::20c:29ff:fe9f:c532%em0 prefixlen 64 scopeid 0x1 + inet6 2001:db8:450a:e723:20c:29ff:fe9f:c532 prefixlen 64 autoconf pltime 86114 vltime 86114 + inet6 fdb6:1d86:d9bd:3:20c:29ff:fe9f:c532 prefixlen 64 autoconf pltime 86114 vltime 86114 + inet 198.51.100.207 netmask 0xffffff00 broadcast 198.51.100.255 + media: Ethernet autoselect (1000baseT ) + status: active + nd6 options=23 +EOF + want_extract_ipv6_global => "2001:db8:450a:e723:20c:29ff:fe9f:c532", + want_ipv6gua_from_if => "2001:db8:450a:e723:20c:29ff:fe9f:c532", + want_ipv6ula_from_if => "fdb6:1d86:d9bd:3:20c:29ff:fe9f:c532", + want_ipv4_from_if => "198.51.100.207", + }, + { name => "ip -4 -o addr show dev eth0 scope global (Buildroot IPv4)", + text => < "198.51.157.237", + }, + { name => "ip -6 -o addr show dev eth0 scope global (Buildroot IPv6)", + text => < "2001:db8:450b:13f:ed44:eb63:b070:212f", + want_ipv6gua_from_if => "2001:db8:450b:13f:ed44:eb63:b070:212f", + }, + { name => "ifconfig eth0 (Busybox)", + text => < "2001:db8:450b:13f:ed44:eb63:b070:212f", + want_ipv6gua_from_if => "2001:db8:450b:13f:ed44:eb63:b070:212f", + want_ipv4_from_if => "198.51.157.237", }, ); From 1b2f45cc597c9376978e336ee290b36b0ed81fc4 Mon Sep 17 00:00:00 2001 From: David Kerr Date: Tue, 4 Aug 2020 18:18:50 -0400 Subject: [PATCH 09/16] Add get_default_interface --- .github/workflows/ci.yml | 3 + ddclient.in | 62 +++++++++- t/get_ip_from_if.pl | 34 +++++ t/lib/ddclient/t.pm | 259 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 356 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4ad62c1..ddf7bb0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,7 @@ jobs: libtest-tcp-perl \ libtest-warnings-perl \ liburi-perl \ + net-tools \ make \ ; - uses: actions/checkout@v2 @@ -117,6 +118,7 @@ jobs: perl-Test-MockModule \ perl-Test-TCP \ perl-Test-Warnings \ + net-tools \ ; - name: autogen run: ./autogen @@ -142,6 +144,7 @@ jobs: perl-HTTP-Daemon \ perl-IO-Socket-INET6 \ perl-core \ + iproute \ ; - name: autogen run: ./autogen diff --git a/ddclient.in b/ddclient.in index 25c1bd1..000dc73 100755 --- a/ddclient.in +++ b/ddclient.in @@ -2705,12 +2705,69 @@ my $regex_ipv6_ula = qr{ $regex_ipv6 # And is a valid IPv6 address }xi; +###################################################################### +## get_default_interface finds the default network interface based on +## the IP routing table on the system. We validate that the interface +## found is likely to have global routing (e.g. is not LOOPBACK). +## Returns undef if no global scope interface can be found for IP version. +###################################################################### +sub get_default_interface { + my $ipver = int(shift // 4); ## Defaults to IPv4 if not specified + my $ipstr = ($ipver == 6) ? 'inet6' : 'inet'; + my $reply = shift // ''; ## Pass in data for unit testing purposes only + my $cmd = "test"; + + return undef if (($ipver != 4) && ($ipver != 6)); + + if (!$reply) { ## skip if test data passed in. + ## Best option is the ip command from iproute2 package + $cmd = "ip -$ipver -o route list match default"; $reply = qx{ $cmd 2>/dev/null }; + ## Fallback is the netstat command. This is only option on MacOS. + if ($?) { $cmd = "netstat -rn -$ipver"; $reply = qx{ $cmd 2>/dev/null }; } # Linux, FreeBSD + if ($?) { $cmd = "netstat -rn -f $ipstr"; $reply = qx{ $cmd 2>/dev/null }; } # MacOS + if ($?) { $cmd = "netstat -rn"; $reply = qx{ $cmd 2>/dev/null }; } # Busybox + if ($?) { $cmd = "missing ip or netstat command"; + failed("Unable to obtain default route information -- %s", $cmd) + } + } + debug("Reply from '%s' :\n------\n%s------", $cmd, $reply); + + # Check we have IPv6 address in case we got routing table from non-specific cmd above + return undef if (($ipver == 6) && !extract_ipv6($reply)); + # Filter down to just the default interfaces + my @list = split(/\n/, $reply); + @list = grep(/^default|^(?:0\.){3}0|^::\/0/, @list); # Select 'default' or '0.0.0.0' or '::/0' + return undef if (scalar(@list) == 0); + debug("Default routes found for IPv%s :\n%s", $ipver, join("\n",@list)); + + # now check each interface to make sure it is global (not loopback). + foreach my $line (@list) { + ## Interface will be after "dev" or the last word in the line. Must accept blank spaces + ## at the end. Interface name may not have any whitespace or forward slash. + $line =~ /\bdev\b\s*\K[^\s\/]+|\b[^\s\/]+(?=[\s\/]*$)/; + my $interface = $&; + ## If test data was passed in skip following tests + if ($cmd ne "test") { + ## We do not want the loopback interface or anything interface without global scope + $cmd = "ip -$ipver -o addr show dev $interface scope global"; $reply = qx{$cmd 2>/dev/null}; + if ($?) { $cmd = "ifconfig $interface"; $reply = qx{$cmd 2>/dev/null}; } + if ($?) { $cmd = "missing ip or ifconfig command"; + failed("Unable to obtain information for '%s' -- %s", $interface, $cmd); + } + debug("Reply from '%s' :\n------\n%s------", $cmd, $reply); + } + ## Has global scope, is not LOOPBACK + return($interface) if (($reply) && ($reply !~ /\bLOOPBACK\b/)); + } + return undef; +} + ###################################################################### ## get_ip_from_interface() finds an IPv4 or IPv6 address from a network ## interface. Defaults to IPv4 unless '6' passed as 2nd parameter. ###################################################################### sub get_ip_from_interface { - my $interface = shift; + my $interface = shift // "default"; my $ipver = int(shift // 4); ## Defaults to IPv4 if not specified my $scope = lc(shift // "gua"); ## "gua" or "ula" my $reply = shift // ''; ## Pass in data for unit testing purposes only @@ -2723,7 +2780,8 @@ sub get_ip_from_interface { return undef; } - if ($reply eq '') { ## skip if test data passed in. + if ((lc($interface) eq "default") && (!$reply)) { ## skip if test data passed in. + $interface = get_default_interface($ipver); return undef if !defined($interface); } diff --git a/t/get_ip_from_if.pl b/t/get_ip_from_if.pl index d4a0c4b..6f08e5d 100644 --- a/t/get_ip_from_if.pl +++ b/t/get_ip_from_if.pl @@ -8,6 +8,19 @@ eval { require 'ddclient'; } or BAIL_OUT($@); #STDOUT->autoflush(1); #$ddclient::globals{'debug'} = 1; +subtest "get_default_interface tests" => sub { + for my $sample (@ddclient::t::routing_samples) { + if (defined($sample->{want_ipv4_if})) { + my $interface = ddclient::get_default_interface(4, $sample->{text}); + is($interface, $sample->{want_ipv4_if}, $sample->{name}); + } + if (defined($sample->{want_ipv6_if})) { + my $interface = ddclient::get_default_interface(6, $sample->{text}); + is($interface, $sample->{want_ipv6_if}, $sample->{name}); + } + } +}; + subtest "get_ip_from_interface tests" => sub { for my $sample (@ddclient::t::interface_samples) { # interface name is undef as we are passing in test data @@ -26,4 +39,25 @@ subtest "get_ip_from_interface tests" => sub { } }; +subtest "Get default interface and IP for test system" => sub { + my $interface = ddclient::get_default_interface(4); + if ($interface) { + isnt($interface, "lo", "Check for loopback 'lo'"); + isnt($interface, "lo0", "Check for loopback 'lo0'"); + my $ip1 = ddclient::get_ip_from_interface("default", 4); + my $ip2 = ddclient::get_ip_from_interface($interface, 4); + is($ip1, $ip2, "Check IPv4 from default interface"); + ok(ddclient::is_ipv4($ip1), "Valid IPv4 from get_ip_from_interface($interface)"); + } + $interface = ddclient::get_default_interface(6); + if ($interface) { + isnt($interface, "lo", "Check for loopback 'lo'"); + isnt($interface, "lo0", "Check for loopback 'lo0'"); + my $ip1 = ddclient::get_ip_from_interface("default", 6); + my $ip2 = ddclient::get_ip_from_interface($interface, 6); + is($ip1, $ip2, "Check IPv6 from default interface"); + ok(ddclient::is_ipv6($ip1), "Valid IPv6 from get_ip_from_interface($interface)"); + } +}; + done_testing(); diff --git a/t/lib/ddclient/t.pm b/t/lib/ddclient/t.pm index 2899e4a..c546b9c 100644 --- a/t/lib/ddclient/t.pm +++ b/t/lib/ddclient/t.pm @@ -301,3 +301,262 @@ EOF want_ipv4_from_if => "198.51.157.237", }, ); + +###################################################################### +## Outputs from ip route and netstat commands to find default route (and therefore interface) +## Samples from Ubuntu 20.04, RHEL8, Buildroot, Busybox, MacOS 10.15, FreeBSD +## NOTE: Any tabs/whitespace at start or end of lines are intentional to match real life data. +###################################################################### +our @routing_samples = ( + { name => "ip -4 -o route list match default (most linux)", + text => < "ens33", + }, + { name => "ip -4 -o route list match default (most linux)", + text => < "ens33", + }, + { name => "ip -4 -o route list match default (buildroot)", + text => < "eth0", + }, + { name => "ip -6 -o route list match default (buildroot)", + text => < "eth0", + }, + { name => "netstat -rn -4 (most linux)", + text => < "ens33", + }, + { name => "netstat -rn -4 (FreeBSD)", + text => < "em0", + }, + { name => "netstat -rn -6 (FreeBSD)", + text => < "em0", + }, + { name => "netstat -rn -6 (most linux)", + text => < "ens33", + }, + { name => "netstat -rn -f inet (MacOS)", + text => < "en0", + }, + { name => "netstat -rn -f inet6 (MacOS)", + text => < "en0", + }, +); From 4c76274ba6e56196ec020000a81cb03120d535e6 Mon Sep 17 00:00:00 2001 From: David Kerr Date: Tue, 4 Aug 2020 18:04:13 -0400 Subject: [PATCH 10/16] Add missing comment block --- ddclient.in | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ddclient.in b/ddclient.in index 000dc73..4ddd505 100755 --- a/ddclient.in +++ b/ddclient.in @@ -2662,6 +2662,10 @@ sub extract_ipv6 { return $ip; } +###################################################################### +## Regex that matches an IPv6 address that is probably globally routable. +## Accepts embedded leading zeros. +###################################################################### my $regex_ipv6_global = qr{ (?! # Is not one of the following addresses: 0{0,4}: # ::/16 is assumed to never contain globaly routable addresses From db8b6baca9f86bf4be53332bb953be7fc7d1b109 Mon Sep 17 00:00:00 2001 From: Krerkkiat Chusap Date: Mon, 5 Apr 2021 12:36:45 -0400 Subject: [PATCH 11/16] modify Cloudflare documentation It seems that ddclient check if the login field is equal to "token" to use the correct header for the API token. --- ddclient.conf.in | 2 +- ddclient.in | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ddclient.conf.in b/ddclient.conf.in index 164a260..b36b514 100644 --- a/ddclient.conf.in +++ b/ddclient.conf.in @@ -192,7 +192,7 @@ ssl=yes # use ssl-support. Works with #protocol=cloudflare, \ #zone=domain.tld, \ #ttl=1, \ -#login=your-login-email, \ # Only needed if you are using your global API key. +#login=your-login-email, \ # Only needed if you are using your global API key. If you are using an API token, set it to "token" (wihtout double quotes). #password=APIKey \ # This is either your global API key, or an API token. If you are using an API token, it must have the permissions "Zone - DNS - Edit" and "Zone - Zone - Read". The Zone resources must be "Include - All zones". #domain.tld,my.domain.tld diff --git a/ddclient.in b/ddclient.in index 4ddd505..1b9e3a9 100755 --- a/ddclient.in +++ b/ddclient.in @@ -4733,6 +4733,7 @@ Example ${program}.conf file entries: ## single host update using an API token protocol=cloudflare, \\ zone=dns.zone, \\ + login=token, \\ password=cloudflare-api-token \\ myhost.com From 9a44eeb82632aafac6d1061e5f4fdc85a105efca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20J=C3=A4ckel?= Date: Tue, 11 May 2021 11:47:10 +0200 Subject: [PATCH 12/16] Use HTTP/1.0, fix #332 --- ddclient.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddclient.in b/ddclient.in index 4ddd505..da21fdb 100755 --- a/ddclient.in +++ b/ddclient.in @@ -2105,7 +2105,7 @@ sub fetch_via_socket_io { } else { $request .= "https://$server" if defined($proxy); } - $request .= "/$url HTTP/1.0\n"; + $request .= "/$url HTTP/1.1\n"; $request .= "Host: $server\n"; if (defined($login) || defined($password)) { From f776018d8264754425e415e56192faddcfdbdd7c Mon Sep 17 00:00:00 2001 From: DaveSophoServices Date: Tue, 11 May 2021 10:15:20 -0500 Subject: [PATCH 13/16] Update ci.yml Removed centos - tests are not running right. --- .github/workflows/ci.yml | 88 ++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ddf7bb0..ebe4d0d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,51 +53,51 @@ jobs: - name: distribution tarball is complete run: ./.github/workflows/scripts/dist-tarball-check - test-centos6: - runs-on: ubuntu-latest - container: centos:6 - steps: - - uses: actions/checkout@v1 - - name: install dependencies - run: | - yum install -y \ - automake \ - perl-IO-Socket-INET6 \ - perl-core \ - perl-libwww-perl \ - ; - - name: autogen - run: ./autogen - - name: configure - run: ./configure - - name: check - run: make VERBOSE=1 AM_COLOR_TESTS=always check - - name: distcheck - run: make VERBOSE=1 AM_COLOR_TESTS=always distcheck + #test-centos6: + # runs-on: ubuntu-latest + # container: centos:6 + # steps: + # - uses: actions/checkout@v1 + # - name: install dependencies + # run: | + # yum install -y \ + # automake \ + # perl-IO-Socket-INET6 \ + # perl-core \ + # perl-libwww-perl \ + # ; + # - name: autogen + # run: ./autogen + # - name: configure + # run: ./configure + # - name: check + # run: make VERBOSE=1 AM_COLOR_TESTS=always check + # - name: distcheck + # run: make VERBOSE=1 AM_COLOR_TESTS=always distcheck - test-centos8: - runs-on: ubuntu-latest - container: centos:8 - steps: - - uses: actions/checkout@v2 - - name: install dependencies - run: | - dnf --refresh --enablerepo=PowerTools install -y \ - automake \ - make \ - perl-HTTP-Daemon \ - perl-IO-Socket-INET6 \ - perl-Test-Warnings \ - perl-core \ - ; - - name: autogen - run: ./autogen - - name: configure - run: ./configure - - name: check - run: make VERBOSE=1 AM_COLOR_TESTS=always check - - name: distcheck - run: make VERBOSE=1 AM_COLOR_TESTS=always distcheck + #test-centos8: + # runs-on: ubuntu-latest + # container: centos:8 + # steps: + # - uses: actions/checkout@v2 + # - name: install dependencies + # run: | + # dnf --refresh --enablerepo=PowerTools install -y \ + # automake \ + # make \ + # perl-HTTP-Daemon \ + # perl-IO-Socket-INET6 \ + # perl-Test-Warnings \ + # perl-core \ + # ; + # - name: autogen + # run: ./autogen + # - name: configure + # run: ./configure + # - name: check + # run: make VERBOSE=1 AM_COLOR_TESTS=always check + # - name: distcheck + # run: make VERBOSE=1 AM_COLOR_TESTS=always distcheck test-fedora: runs-on: ubuntu-latest From 9fb2aee4d0ec1890d78de13cb43d11146d9f6238 Mon Sep 17 00:00:00 2001 From: Mike Chester Date: Tue, 11 May 2021 08:48:44 -0700 Subject: [PATCH 14/16] Fix geturl function call for Gandi (#314) * Fix geturl function call The function should be called without the brackets * Update ci.yml Removed Cento6 & 8 Co-authored-by: DaveSophoServices --- ddclient.in | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ddclient.in b/ddclient.in index 23776af..445979f 100755 --- a/ddclient.in +++ b/ddclient.in @@ -5744,13 +5744,13 @@ sub nic_gandi_update { $url = "https://$config{$h}{'server'}$config{$h}{'script'}"; $url .= "/livedns/domains/$config{$h}{'zone'}/records/$hostname/$rrset_type"; - my $reply = geturl({ - proxy => opt('proxy'), - url => $url, - headers => $headers, - method => 'PUT', - data => $data, - }); + my $reply = geturl( + proxy => opt('proxy'), + url => $url, + headers => $headers, + method => 'PUT', + data => $data, + ); unless ($reply) { failed("%s -- Could not connect to %s.", $h, $config{$h}{'server'}); next; From 24ba9459491197fe045934a3e7fb54655812cb25 Mon Sep 17 00:00:00 2001 From: David Kerr Date: Tue, 11 May 2021 21:16:19 -0400 Subject: [PATCH 15/16] IPv6 framework + Cloudflare + FreeDNS (#291) * Add basic framework to support IPv6 * Update cloudflare to use new IPv6 framework * Update FreeDNS to use new IPv6 framework --- ddclient.in | 993 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 817 insertions(+), 176 deletions(-) diff --git a/ddclient.in b/ddclient.in index 445979f..8344e44 100755 --- a/ddclient.in +++ b/ddclient.in @@ -75,6 +75,9 @@ my ($result, %config, %cache); my $saved_cache; my %saved_opt; my $daemon; +# Control how many times warning message logged for invalid IP addresses +my (%warned_ip, %warned_ipv4, %warned_ipv6); +my $inv_ip_warn_count = opt('max-warn') // 1; sub T_ANY { 'any' } sub T_STRING { 'string' } @@ -90,9 +93,13 @@ sub T_FILE { 'file name' } sub T_FQDNP { 'fully qualified host name and optional port number' } sub T_PROTO { 'protocol' } sub T_USE { 'ip strategy' } +sub T_USEV4 { 'ipv4 strategy' } +sub T_USEV6 { 'ipv6 strategy' } sub T_IF { 'interface' } sub T_PROG { 'program name' } sub T_IP { 'ip' } +sub T_IPV4 { 'ipv4' } +sub T_IPV6 { 'ipv6' } sub T_POSTS { 'postscript' } ## strategies for obtaining an ip address. @@ -352,14 +359,15 @@ my %builtinfw = ( ); my %ip_strategies = ( - 'ip' => ": use IP given by '-ip
'", - 'web' => ": obtain IP from the web-based IP discovery service given by '-web |'", - 'fw' => ": obtain IP from a firewall/router device by visiting the URL given by '-fw '", - 'if' => ": obtain IP from the interface given by '-if '", - 'cmd' => ": obtain IP by running the command given by '-cmd '", - 'cisco' => ": obtain IP from Cisco FW device at the address given by '-fw
'", - 'cisco-asa' => ": obtain IP from Cisco ASA device at the address given by '-fw
'", - map({ $_ => sprintf(": obtain IP from %s device at the address given by '-fw
'", + 'no' => ": deprecated, see 'usev4' and 'usev6'", + 'ip' => ": deprecated, see 'usev4' and 'usev6'", + 'web' => ": deprecated, see 'usev4' and 'usev6'", + 'fw' => ": deprecated, see 'usev4' and 'usev6'", + 'if' => ": deprecated, see 'usev4' and 'usev6'", + 'cmd' => ": deprecated, see 'usev4' and 'usev6'", + 'cisco' => ": deprecated, see 'usev4' and 'usev6'", + 'cisco-asa' => ": deprecated, see 'usev4' and 'usev6'", + map({ $_ => sprintf(": Built-in firewall %s deprecated, see 'usev4' and 'usev6'", $builtinfw{$_}->{'name'}) } keys(%builtinfw)), ); @@ -369,6 +377,41 @@ sub ip_strategies_usage { ('ip', 'web', 'if', 'cmd', 'fw', sort('cisco', 'cisco-asa', keys(%builtinfw)))); } +my %ipv4_strategies = ( + 'disabled' => ": do not obtain an IPv4 address for this host", + 'ipv4' => ": obtain IPv4 from -ipv4 {address}", + 'webv4' => ": obtain IPv4 from an IP discovery page on the web", + 'ifv4' => ": obtain IPv4 from the -ifv4 {interface}", + 'cmdv4' => ": obtain IPv4 from the -cmdv4 {external-command}", + 'fwv4' => ": obtain IPv4 from the firewall specified by -fwv4 {type|address}", + 'ciscov4' => ": obtain IPv4 from Cisco FW at the -fwv4 {address}", + 'cisco-asav4' => ": obtain IPv4 from Cisco ASA at the -fwv4 {address}", + map { $_ => sprintf ": obtain IPv4 from %s at the -fwv4 {address}", $builtinfw{$_}->{'name'} } keys %builtinfw, +); +sub ipv4_strategies_usage { + return map { sprintf(" -usev4=%-22s %s.", $_, $ipv4_strategies{$_}) } sort keys %ipv4_strategies; +} + +my %ipv6_strategies = ( + 'no' => ": deprecated, use 'disabled'", + 'disabled' => ": do not obtain an IPv6 address for this host", + 'ip' => ": deprecated, use 'ipv6'", + 'ipv6' => ": obtain IPv6 from -ipv6 {address}", + 'web' => ": deprecated, use 'webv6'", + 'webv6' => ": obtain IPv6 from an IP discovery page on the web", + 'if' => ": deprecated, use 'ifv6'", + 'ifv6' => ": obtain IPv6 from the -if {interface}", + 'cmd' => ": deprecated, use 'cmdv6'", + 'cmdv6' => ": obtain IPv6 from the -cmdv6 {external-command}", + 'fwv6' => ": obtain IPv6 from the firewall specified by -fwv6 {type|address}", + 'ciscov6' => ": obtain IPv6 from Cisco FW at the -fwv6 {address}", + 'cisco-asav6' => ": obtain IPv6 from Cisco ASA at the -fwv6 {address}", + map { $_ => sprintf ": obtain IPv6 from %s at the -fwv6 {address}", $builtinfw{$_}->{'name'} } keys %builtinfw, +); +sub ipv6_strategies_usage { + return map { sprintf(" -usev6=%-22s %s.", $_, $ipv6_strategies{$_}) } sort keys %ipv6_strategies; +} + sub setv { return { 'type' => shift, @@ -389,28 +432,44 @@ my %variables = ( 'protocol' => setv(T_PROTO, 0, 0, 'dyndns2', undef), 'use' => setv(T_USE, 0, 0, 'ip', undef), + 'usev4' => setv(T_USEV4, 0, 0, 'disabled', undef), + 'usev6' => setv(T_USEV6, 0, 0, 'disabled', undef), 'ip' => setv(T_IP, 0, 0, undef, undef), + 'ipv4' => setv(T_IPV4, 0, 0, undef, undef), + 'ipv6' => setv(T_IPV6, 0, 0, undef, undef), 'if' => setv(T_IF, 0, 0, 'ppp0', undef), + 'ifv4' => setv(T_IF, 0, 0, 'default', undef), + 'ifv6' => setv(T_IF, 0, 0, 'default', undef), 'web' => setv(T_STRING,0, 0, 'dyndns', undef), 'web-skip' => setv(T_STRING,1, 0, '', undef), + 'webv4' => setv(T_STRING,0, 0, 'googledomains', undef), + 'webv4-skip' => setv(T_STRING,1, 0, '', undef), + 'webv6' => setv(T_STRING,0, 0, 'googledomains', undef), + 'webv6-skip' => setv(T_STRING,1, 0, '', undef), 'fw' => setv(T_ANY, 0, 0, '', undef), 'fw-skip' => setv(T_STRING,1, 0, '', undef), + 'fwv4' => setv(T_ANY, 0, 0, '', undef), + 'fwv4-skip' => setv(T_STRING,1, 0, '', undef), + 'fwv6' => setv(T_ANY, 0, 0, '', undef), + 'fwv6-skip' => setv(T_STRING,1, 0, '', undef), 'fw-login' => setv(T_LOGIN, 1, 0, '', undef), 'fw-password' => setv(T_PASSWD,1, 0, '', undef), 'cmd' => setv(T_PROG, 0, 0, '', undef), 'cmd-skip' => setv(T_STRING,1, 0, '', undef), + 'cmdv4' => setv(T_PROG, 0, 0, '', undef), + 'cmdv6' => setv(T_PROG, 0, 0, '', undef), 'timeout' => setv(T_DELAY, 0, 0, interval('120s'), interval('120s')), 'retry' => setv(T_BOOL, 0, 0, 0, undef), 'force' => setv(T_BOOL, 0, 0, 0, undef), 'ssl' => setv(T_BOOL, 0, 0, 0, undef), 'curl' => setv(T_BOOL, 0, 0, 0, undef), - 'ipv6' => setv(T_BOOL, 0, 0, 0, undef), 'syslog' => setv(T_BOOL, 0, 0, 0, undef), 'facility' => setv(T_STRING,0, 0, 'daemon', undef), 'priority' => setv(T_STRING,0, 0, 'notice', undef), 'mail' => setv(T_EMAIL, 0, 0, '', undef), 'mail-failure' => setv(T_EMAIL, 0, 0, '', undef), + 'max-warn' => setv(T_NUMBER,0, 0, 1, undef), 'exec' => setv(T_BOOL, 0, 0, 1, undef), 'debug' => setv(T_BOOL, 0, 0, 0, undef), @@ -442,12 +501,15 @@ my %variables = ( 'fw-ssl-validate' => setv(T_BOOL, 0, 0, 1, undef), 'cmd' => setv(T_PROG, 0, 0, '', undef), 'cmd-skip' => setv(T_STRING,0, 0, '', undef), - 'ipv6' => setv(T_BOOL, 0, 0, 0, undef), - 'ip' => setv(T_IP, 0, 1, undef, undef), + 'ip' => setv(T_IP, 0, 1, undef, undef), #TODO remove from cache? + 'ipv4' => setv(T_IPV4, 0, 1, undef, undef), + 'ipv6' => setv(T_IPV6, 0, 1, undef, undef), 'wtime' => setv(T_DELAY, 0, 1, 0, interval('30s')), 'mtime' => setv(T_NUMBER,0, 1, 0, undef), 'atime' => setv(T_NUMBER,0, 1, 0, undef), - 'status' => setv(T_ANY, 0, 1, '', undef), + 'status' => setv(T_ANY, 0, 1, '', undef), #TODO remove from cache? + 'status-ipv4' => setv(T_ANY, 0, 1, '', undef), + 'status-ipv6' => setv(T_ANY, 0, 1, '', undef), 'min-interval' => setv(T_DELAY, 0, 0, interval('30s'), 0), 'max-interval' => setv(T_DELAY, 0, 0, interval('25d'), 0), 'min-error-interval' => setv(T_DELAY, 0, 0, interval('5m'), 0), @@ -784,28 +846,46 @@ my @opt = ( ["cache", "=s", "-cache : record address used in "], ["pid", "=s", "-pid : record process id in if daemonized"], "", - ["use", "=s", "-use : how the IP address should be obtained"], + ["use", "=s", "-use : deprecated, see 'usev4' and 'usev6'"], &ip_strategies_usage(), + [ "usev4", "=s", "-usev4 : how the should IPv4 address be obtained."], + &ipv4_strategies_usage(), + [ "usev6", "=s", "-usev6 : how the should IPv6 address be obtained."], + &ipv6_strategies_usage(), "", " Options that apply to 'use=ip':", - ["ip", "=s", "-ip
: set the IP address to
"], + ["ip", "=s", "-ip
: deprecated, use 'ipv4' or 'ipv6'"], + ["ipv4", "=s", "-ipv4
: set the IPv4 address to
"], + ["ipv6", "=s", "-ipv6
: set the IPv6 address to
"], "", " Options that apply to 'use=if':", - ["if", "=s", "-if : obtain IP address from "], + ["if", "=s", "-if : deprecated, use 'ifv4' or 'ifv6'"], + ["ifv4", "=s", "-ifv4 : obtain IPv4 address from "], + ["ifv6", "=s", "-ifv6 : obtain IPv6 address from "], "", " Options that apply to 'use=web':", - ["web", "=s", "-web | : obtain IP address from a web-based IP discovery service, either a known or a custom "], - ["web-skip", "=s", "-web-skip : skip any IP addresses before in the text returned from the web-based IP discovery service"], + ["web", "=s", "-web | : deprecated, use 'webv4' or 'webv6'"], + ["web-skip", "=s", "-web-skip : deprecated, use 'webv4-skip' or 'webv6-skip'"], + ["webv4", "=s", "-webv4 |: obtain IPv4 address from a web-based IP discovery service, either a known or a custom "], + ["webv4-skip", "=s", "-webv4-skip : skip any IP addresses before in the output of 'ip address show dev ' (or 'ifconfig ')"], + ["webv6", "=s", "-webv6 |: obtain IPv6 address from a web-based IP discovery service, either a known or a custom "], + ["webv6-skip", "=s", "-webv6-skip : skip any IP addresses before in the output of 'ip address show dev ' (or 'ifconfig ')"], "", " Options that apply to 'use=fw' and 'use=':", - ["fw", "=s", "-fw
| : obtain IP address from device with IP address
or URL "], - ["fw-skip", "=s", "-fw-skip : skip any IP addresses before in the text returned from the device"], + ["fw", "=s", "-fw
| : deprecated, use 'fwv4' or 'fwv6'"], + ["fw-skip", "=s", "-fw-skip : deprecated, use 'fwv4-skip' or 'fwv6-skip'"], + ["fwv4", "=s", "-fwv4
| : obtain IPv4 address from device with IP address
or URL "], + ["fwv4-skip", "=s", "-fwv4-skip : skip any IP addresses before in the text returned from the device"], + ["fwv6", "=s", "-fwv6
| : obtain IPv6 address from device with IP address
or URL "], + ["fwv6-skip", "=s", "-fwv6-skip : skip any IP addresses before in the text returned from the device"], ["fw-login", "=s", "-fw-login : use when getting the IP from the device"], ["fw-password", "=s", "-fw-password : use password when getting the IP from the device"], "", " Options that apply to 'use=cmd':", - ["cmd", "=s", "-cmd : obtain IP address from the output of "], - ["cmd-skip", "=s", "-cmd-skip : skip any IP addresses before in the command's output"], + ["cmd", "=s", "-cmd : deprecated, use 'cmdv4' or 'cmdv6'"], + ["cmd-skip", "=s", "-cmd-skip : deprecated, filter in program wrapper script"], + ["cmdv4", "=s", "-cmdv4 : obtain IPv4 address from the output of "], + ["cmdv6", "=s", "-cmdv6 : obtain IPv6 address from the output of "], "", ["login", "=s", "-login : log in to the dynamic DNS service as "], ["password", "=s", "-password : log in to the dynamic DNS service with password "], @@ -825,13 +905,13 @@ my @opt = ( ["syslog", "!", "-{no}syslog : log messages to syslog"], ["facility", "=s", "-facility : log messages to syslog to facility "], ["priority", "=s", "-priority : log messages to syslog with priority "], + ["max-warn", "=i", "-max-warn : log at most warning messages for undefined IP address"], ["mail", "=s", "-mail
: e-mail messages to
"], ["mail-failure", "=s", "-mail-failure : e-mail messages for failed updates to "], ["exec", "!", "-{no}exec : do {not} execute; just show what would be done"], ["debug", "!", "-{no}debug : print {no} debugging information"], ["verbose", "!", "-{no}verbose : print {no} verbose information"], ["quiet", "!", "-{no}quiet : print {no} messages for unnecessary updates"], - ["ipv6", "!", "-{no}ipv6 : use ipv6"], ["help", "", "-help : display this message and exit"], ["postscript", "", "-postscript : script to run after updating ddclient, has new IP as param"], ["query", "!", "-{no}query : print {no} ip addresses and exit"], @@ -907,6 +987,10 @@ sub main { fatal("invalid argument '-use %s'; possible values are:\n%s", $opt{'use'}, join("\n", ip_strategies_usage())) unless exists $ip_strategies{lc opt('use')}; + if (defined($opt{'usev6'})) { + usage("invalid argument '-usev6 %s'; possible values are:\n%s", $opt{'usev6'}, join("\n",ipv6_strategies_usage())) + unless exists $ipv6_strategies{lc opt('usev6')}; + } $daemon = opt('daemon'); @@ -968,9 +1052,11 @@ sub runpostscript { sub update_nics { my %examined = (); my %iplist = (); + my %ipv4list = (); + my %ipv6list = (); foreach my $s (sort keys %services) { - my (@hosts, %ips) = (); + my (@hosts, %ipsv4, %ipsv6) = (); my $updateable = $services{$s}{'updateable'}; my $update = $services{$s}{'update'}; @@ -978,33 +1064,103 @@ sub update_nics { next if $config{$h}{'protocol'} ne lc($s); $examined{$h} = 1; # we only do this once per 'use' and argument combination - my $use = opt('use', $h); - my $arg_ip = opt('ip', $h) // ''; - my $arg_fw = opt('fw', $h) // ''; - my $arg_if = opt('if', $h) // ''; - my $arg_web = opt('web', $h) // ''; - my $arg_cmd = opt('cmd', $h) // ''; - my $ip = ""; - if (exists $iplist{$use}{$arg_ip}{$arg_fw}{$arg_if}{$arg_web}{$arg_cmd}) { - $ip = $iplist{$use}{$arg_ip}{$arg_fw}{$arg_if}{$arg_web}{$arg_cmd}; - } else { - $ip = get_ip($use, $h); - if (!defined($ip)) { - warning("unable to determine IP address") - if !$daemon || opt('verbose'); - next; + my $use = opt('use', $h) // 'disabled'; + my $usev4 = opt('usev4', $h) // 'disabled'; + my $usev6 = opt('usev6', $h) // 'disabled'; + $use = 'disabled' if ($use eq 'no'); # backward compatibility + $usev6 = 'disabled' if ($usev6 eq 'no'); # backward compatibility + my $arg_ip = opt('ip', $h) // ''; + my $arg_ipv4 = opt('ipv4', $h) // ''; + my $arg_ipv6 = opt('ipv6', $h) // ''; + my $arg_fw = opt('fw', $h) // ''; + my $arg_fwv4 = opt('fwv4', $h) // ''; + my $arg_fwv6 = opt('fwv6', $h) // ''; + my $arg_if = opt('if', $h) // ''; + my $arg_ifv4 = opt('ifv4', $h) // ''; + my $arg_ifv6 = opt('ifv6', $h) // ''; + my $arg_web = opt('web', $h) // ''; + my $arg_webv4 = opt('webv4', $h) // ''; + my $arg_webv6 = opt('webv6', $h) // ''; + my $arg_cmd = opt('cmd', $h) // ''; + my $arg_cmdv4 = opt('cmdv4', $h) // ''; + my $arg_cmdv6 = opt('cmdv6', $h) // ''; + my $ip = undef; + my $ipv4 = undef; + my $ipv6 = undef; + + if ($use ne 'disabled') { + if (exists $iplist{$use}{$arg_ip}{$arg_fw}{$arg_if}{$arg_web}{$arg_cmd}) { + # If we have already done a get_ip() for this, don't do it again. + $ip = $iplist{$use}{$arg_ip}{$arg_fw}{$arg_if}{$arg_web}{$arg_cmd}; + } else { + # Else need to find the IP address... + $ip = get_ip($use, $h); + if (is_ipv4($ip) || is_ipv6($ip)) { + # And if it is valid, remember it... + $iplist{$use}{$arg_ip}{$arg_fw}{$arg_if}{$arg_web}{$arg_cmd} = $ip; + } else { + warning("%s: unable to determine IP address with strategy use=%s", $h, $use) + if !$daemon || opt('verbose'); + } } - $iplist{$use}{$arg_ip}{$arg_fw}{$arg_if}{$arg_web}{$arg_cmd} = $ip; + # And remember it as the IP address we want to send to the DNS service. + $config{$h}{'wantip'} = $ip; } - $config{$h}{'wantip'} = $ip; + + if ($usev4 ne 'disabled') { + if (exists $ipv4list{$usev4}{$arg_ipv4}{$arg_fwv4}{$arg_ifv4}{$arg_webv4}{$arg_cmdv4}) { + # If we have already done a get_ipv4() for this, don't do it again. + $ipv4 = $ipv4list{$usev4}{$arg_ipv4}{$arg_fwv4}{$arg_ifv4}{$arg_webv4}{$arg_cmdv4}; + } else { + # Else need to find the IPv4 address... + $ipv4 = get_ipv4($usev4, $h); + if (is_ipv4($ipv4)) { + # And if it is valid, remember it... + $ipv4list{$usev4}{$arg_ipv4}{$arg_fwv4}{$arg_ifv4}{$arg_webv4}{$arg_cmdv4} = $ipv4; + } else { + warning("%s: unable to determine IPv4 address with strategy usev4=%s", $h, $usev4) + if !$daemon || opt('verbose'); + } + } + # And remember it as the IPv4 address we want to send to the DNS service. + $config{$h}{'wantipv4'} = $ipv4; + } + + if ($usev6 ne 'disabled') { + if (exists $ipv6list{$usev6}{$arg_ipv6}{$arg_fwv6}{$arg_ifv6}{$arg_webv6}{$arg_cmdv6}) { + # If we have already done a get_ipv6() for this, don't do it again. + $ipv6 = $ipv6list{$usev6}{$arg_ipv6}{$arg_fwv6}{$arg_ifv6}{$arg_webv6}{$arg_cmdv6}; + } else { + # Else need to find the IPv6 address... + $ipv6 = get_ipv6($usev6, $h); + if (is_ipv6($ipv6)) { + # And if it is valid, remember it... + $ipv6list{$usev6}{$arg_ipv6}{$arg_fwv6}{$arg_ifv6}{$arg_webv6}{$arg_cmdv6} = $ipv6; + } else { + warning("%s: unable to determine IPv6 address with strategy usev6=%s", $h, $usev6) + if !$daemon || opt('verbose'); + } + } + # And remember it as the IP address we want to send to the DNS service. + $config{$h}{'wantipv6'} = $ipv6; + } + + # DNS service update functions should only have to handle 'wantipv4' and 'wantipv6' + $config{$h}{'wantipv4'} = $ipv4 = $ip if (!$ipv4 && is_ipv4($ip)); + $config{$h}{'wantipv6'} = $ipv6 = $ip if (!$ipv6 && is_ipv6($ip)); + # But we will set 'wantip' to the IPv4 so old functions continue to work until we update them all + $config{$h}{'wantip'} = $ipv4 if (!$ip && $ipv4); + next if !nic_updateable($h, $updateable); push @hosts, $h; - $ips{$ip} = $h; + + $ipsv4{$ipv4} = $h if ($ipv4); + $ipsv6{$ipv6} = $h if ($ipv6); } if (@hosts) { $0 = sprintf("%s - updating %s", $program, join(',', @hosts)); &$update(@hosts); - runpostscript(join ' ', keys %ips); + runpostscript(join ' ', keys %ipsv4, keys %ipsv6); } } foreach my $h (sort keys %config) { @@ -1052,8 +1208,7 @@ sub write_cache { ## merge the updated host entries into the cache. foreach my $h (keys %config) { if (!exists $cache{$h} || $config{$h}{'update'}) { - map { $cache{$h}{$_} = $config{$h}{$_} } @{$config{$h}{'cacheable'}}; - + map { defined($config{$h}{$_}) ? ($cache{$h}{$_} = $config{$h}{$_}) : () } @{$config{$h}{'cacheable'}}; } else { map { $cache{$h}{$_} = $config{$h}{$_} } qw(atime wtime status); } @@ -1341,10 +1496,22 @@ sub init_config { $opt{'quiet'} = 0 if opt('verbose'); ## infer the IP strategy if possible - if (!defined($opt{'use'})) { - $opt{'use'} = 'web' if defined($opt{'web'}); - $opt{'use'} = 'if' if defined($opt{'if'}); - $opt{'use'} = 'ip' if defined($opt{'ip'}); + if (!$opt{'use'}) { + $opt{'use'} = 'web' if ($opt{'web'}); + $opt{'use'} = 'if' if ($opt{'if'}); + $opt{'use'} = 'ip' if ($opt{'ip'}); + } + ## infer the IPv4 strategy if possible + if (!$opt{'usev4'}) { + $opt{'usev4'} = 'webv4' if ($opt{'webv4'}); + $opt{'usev4'} = 'ifv4' if ($opt{'ifv4'}); + $opt{'usev4'} = 'ipv4' if ($opt{'ipv4'}); + } + ## infer the IPv6 strategy if possible + if (!$opt{'usev6'}) { + $opt{'usev6'} = 'webv6' if ($opt{'webv6'}); + $opt{'usev6'} = 'ifv6' if ($opt{'ifv6'}); + $opt{'usev6'} = 'ipv6' if ($opt{'ipv6'}); } ## sanity check @@ -1532,6 +1699,7 @@ sub process_args { sub test_possible_ip { local $opt{'debug'} = 0; + printf "----- Test_possible_ip with 'get_ip' -----\n"; printf "use=ip, ip=%s address is %s\n", opt('ip'), get_ip('ip') // 'NOT FOUND' if defined opt('ip'); @@ -1575,6 +1743,75 @@ sub test_possible_ip { local $opt{'use'} = 'cmd'; printf "use=cmd, cmd=%s address is %s\n", opt('cmd'), get_ip('cmd') // 'NOT FOUND'; } + + # Now force IPv4 + printf "----- Test_possible_ip with 'get_ipv4' ------\n"; + printf "use=ipv4, ipv4=%s address is %s\n", opt('ipv4'), get_ipv4('ipv4') // 'NOT FOUND' + if defined opt('ipv4'); + + { + # Note: The `ip` command adds a `@eth0` suffix to the names of VLAN + # interfaces. That `@eth0` suffix is NOT part of the interface name. + my @ifs = map({ /^[^\s:]*:\s*([^\s:@]+)/ ? $1 : () } + `command -v ip >/dev/null && ip -o link show`); + @ifs = map({ /^([a-zA-Z].*?)(?::?\s.*)?$/ ? $1 : () } + `command -v ifconfig >/dev/null && ifconfig -a`) if $? || !@ifs; + @ifs = () if $?; + warning("failed to get list of interfaces") if !@ifs; + foreach my $if (@ifs) { + local $opt{'ifv4'} = $if; + printf "use=ifv4, ifv4=%s address is %s\n", opt('ifv4'), get_ipv4('ifv4') // 'NOT FOUND'; + } + } + { + local $opt{'usev4'} = 'webv4'; + foreach my $web (sort keys %builtinweb) { + local $opt{'webv4'} = $web; + printf "use=webv4, webv4=$web address is %s\n", get_ipv4('webv4') // 'NOT FOUND' + if ($web !~ "6") ## Don't bother if web site only supports IPv6; + } + printf "use=webv4, webv4=%s address is %s\n", opt('webv4'), get_ipv4('webv4') // 'NOT FOUND' + if ! exists $builtinweb{opt('webv4')}; + } + if (opt('cmdv4')) { + local $opt{'usev4'} = 'cmdv4'; + printf "use=cmdv4, cmdv4=%s address is %s\n", opt('cmdv4'), get_ipv4('cmdv4') // 'NOT FOUND'; + } + + # Now force IPv6 + printf "----- Test_possible_ip with 'get_ipv6' -----\n"; + printf "use=ipv6, ipv6=%s address is %s\n", opt('ipv6'), get_ipv6('ipv6') // 'NOT FOUND' + if defined opt('ipv6'); + + { + # Note: The `ip` command adds a `@eth0` suffix to the names of VLAN + # interfaces. That `@eth0` suffix is NOT part of the interface name. + my @ifs = map({ /^[^\s:]*:\s*([^\s:@]+)/ ? $1 : () } + `command -v ip >/dev/null && ip -o link show`); + @ifs = map({ /^([a-zA-Z].*?)(?::?\s.*)?$/ ? $1 : () } + `command -v ifconfig >/dev/null && ifconfig -a`) if $? || !@ifs; + @ifs = () if $?; + warning("failed to get list of interfaces") if !@ifs; + foreach my $if (@ifs) { + local $opt{'ifv6'} = $if; + printf "use=ifv6, ifv6=%s address is %s\n", opt('ifv6'), get_ipv6('ifv6') // 'NOT FOUND'; + } + } + { + local $opt{'usev6'} = 'webv6'; + foreach my $web (sort keys %builtinweb) { + local $opt{'webv6'} = $web; + printf "use=webv6, webv6=$web address is %s\n", get_ipv6('webv6') // 'NOT FOUND' + if ($web !~ "4"); ## Don't bother if web site only supports IPv4 + } + printf "use=webv6, webv6=%s address is %s\n", opt('webv6'), get_ipv6('webv6') // 'NOT FOUND' + if ! exists $builtinweb{opt('webv6')}; + } + if (opt('cmdv6')) { + local $opt{'usev6'} = 'cmdv6'; + printf "use=cmdv6, cmdv6=%s address is %s\n", opt('cmdv6'), get_ipv6('cmdv6') // 'NOT FOUND'; + } + exit 0 unless opt('debug'); } ###################################################################### @@ -1942,6 +2179,14 @@ sub check_value { $value = lc $value; return undef if !exists $ip_strategies{$value}; + } elsif ($type eq T_USEV4) { + $value = lc $value; + return undef if ! exists $ipv4_strategies{$value}; + + } elsif ($type eq T_USEV6) { + $value = lc $value; + return undef if ! exists $ipv6_strategies{$value}; + } elsif ($type eq T_FILE) { return undef if $value eq ""; @@ -1956,6 +2201,13 @@ sub check_value { } elsif ($type eq T_IP) { return undef if !is_ipv4($value) && !is_ipv6($value); + + } elsif ($type eq T_IPV4) { + return undef if !is_ipv4($value); + + } elsif ($type eq T_IPV6) { + return undef if !is_ipv6($value); + } return $value; } @@ -2448,6 +2700,7 @@ sub fetch_via_curl { ###################################################################### sub get_ip { my $use = lc shift; + $use = 'disabled' if ($use eq 'no'); # backward compatibility my $h = shift; my ($ip, $arg, $reply, $url, $skip) = (undef, opt($use, $h), ''); $arg = '' unless $arg; @@ -2455,7 +2708,7 @@ sub get_ip { if ($use eq 'ip') { $ip = opt('ip', $h); if (!is_ipv4($ip) && !is_ipv6($ip)) { - warning("'%s' is not a valid IPv4 or IPv6 address", $ip); + warning("'%s' is not a valid IPv4 or IPv6 address", $ip // ''); $ip = undef; } $arg = 'ip'; @@ -2534,6 +2787,10 @@ sub get_ip { ) // ''; $arg = $url; + } elsif ($use eq 'disabled') { + ## This is a no-op... Do not get an IP address for this host/service + $reply = ''; + } else { $url = opt('fw', $h) // ''; $skip = opt('fw-skip', $h) // ''; @@ -2571,7 +2828,6 @@ sub get_ip { return $ip; } - ###################################################################### ## Regex to find IPv4 address. Accepts embedded leading zeros. ###################################################################### @@ -2873,10 +3129,242 @@ sub get_ip_from_interface { return extract_ipv6($sorted[0]); } +###################################################################### +## get_ipv4 +###################################################################### +sub get_ipv4 { + my $usev4 = lc(shift); ## Method to obtain IP address + my $h = shift; ## Host/service making the request + + my $ipv4 = undef; ## Found IPv4 address + my $reply = ''; ## Text returned from various methods + my $url = ''; ## URL of website or firewall + my $skip = ''; ## Regex of pattern to skip before looking for IP + my $arg = opt($usev4, $h) // ''; ## Value assigned to the "usev4" method + + if ($usev4 eq 'ipv4') { + ## Static IPv4 address is provided in "ipv4=
" + $ipv4 = $arg; + if (!is_ipv4($ipv4)) { + warning("'%s' is not a valid IPv4",$ipv4 // ''); + $ipv4 = undef; + } + $arg = 'ipv4'; # For debug message at end of function + + } elsif ($usev4 eq 'ifv4') { + ## Obtain IPv4 address from interface mamed in "ifv4=" + warning("'if-skip' is deprecated and does nothing for IPv4") if (opt('verbose') && opt('if-skip', $h)); + $ipv4 = get_ip_from_interface($arg,4); + + } elsif ($usev4 eq 'cmdv4') { + ## Obtain IPv4 address by executing the command in "cmdv4=" + warning("'cmd-skip' is deprecated and does nothing for IPv4") if (opt('verbose') && opt('cmd-skip', $h)); + if ($arg) { + my $sys_cmd = quotemeta($arg); + $reply = qx{$sys_cmd}; + $reply = '' if $?; + } + + } elsif ($usev4 eq 'webv4') { + ## Obtain IPv4 address by accessing website at url in "webv4=" + $url = $arg; + $skip = opt('webv4-skip', $h) // ''; + if (exists $builtinweb{$url}) { + $skip = $builtinweb{$url}->{'skip'} unless $skip; + $url = $builtinweb{$url}->{'url'}; + $arg = $url; + } + if ($url) { + $reply = geturl( proxy => opt('proxy', $h), + url => $url, + ipversion => 4, # when using a URL to find IPv4 address we should force use of IPv4 + ssl_validate => opt('ssl-validate', $h), + ) // ''; + } + + } elsif ($usev4 eq 'cisco' || $usev4 eq 'cisco-asa') { + # Stuff added to support Cisco router ip http or ASA https daemon + # User fw-login should only have level 1 access to prevent + # password theft. This is pretty harmless. + warning("'if' does nothing for IPv4. Use 'ifv4'") if (opt('if', $h)); + warning("'fw' does nothing for IPv4. Use 'fwv4'") if (opt('fw', $h)); + warning("'fw-skip' does nothing for IPv4. Use 'fwv4-skip'") if (opt('fw-skip', $h)); + my $queryif = opt('ifv4', $h) // opt('if', $h); + $skip = opt('fwv4-skip', $h) // opt('fw-skip', $h) // ''; + # Convert slashes to protected value "\/" + $queryif =~ s%\/%\\\/%g; + # Protect special HTML characters (like '?') + $queryif =~ s/([\?&= ])/sprintf("%%%02x", ord($1))/ge; + if ($usev4 eq 'cisco') { + $url = "http://" . (opt('fwv4', $h) // opt('fw', $h)) . "/level/1/exec/show/ip/interface/brief/${queryif}/CR"; + } else { + $url = "https://" . (opt('fwv4', $h) // opt('fw', $h)) . "/exec/show%20interface%20${queryif}"; + } + $arg = $url; + $reply = geturl( + url => $url, + login => opt('fw-login', $h), + password => opt('fw-password', $h), + ipversion => 4, # when using a URL to find IPv4 address we should force use of IPv4 + ignore_ssl_option => 1, + ssl_validate => opt('ssl-validate', $h), + ) // ''; + + } elsif ($usev4 eq 'disabled') { + ## This is a no-op... Do not get an IPv4 address for this host/service + $reply = ''; + + } else { + warning("'fw' does nothing for IPv4. Use 'fwv4'") if (opt('fw', $h)); + warning("'fw-skip' does nothing for IPv4. Use 'fwv4-skip'") if (opt('fw-skip', $h)); + $url = opt('fwv4', $h) // opt('fw', $h) // ''; + $skip = opt('fwv4-skip', $h) // opt('fw-skip', $h) // ''; + + if (exists $builtinfw{$usev4}) { + $skip = $builtinfw{$usev4}->{'skip'} unless $skip; + $url = "http://${url}" . $builtinfw{$usev4}->{'url'} unless $url =~ /\//; + } + $arg = $url; + if ($url) { + $reply = geturl( + url => $url, + login => opt('fw-login', $h), + password => opt('fw-password', $h), + ipversion => 4, # when using a URL to find IPv4 address we should force use of IPv4 + ignore_ssl_option => 1, + ssl_validate => opt('ssl-validate', $h), + ) // ''; + } + } + + ## Set to loopback address if no text set yet + $reply = '0.0.0.0' if !defined($reply); + if (($skip // '') ne '') { + $skip =~ s/ /\\s/is; + $reply =~ s/^.*?${skip}//is; + } + ## If $ipv4 not set yet look for IPv4 address in the $reply text + $ipv4 //= extract_ipv4($reply); + ## Return undef for loopback address unless statically assigned by "ipv4=0.0.0.0" + $ipv4 = undef if (($usev4 ne 'ipv4') && (($ipv4 // '') eq '0.0.0.0')); + debug("get_ipv4: using (%s, %s) reports %s", $usev4, $arg, $ipv4 // ""); + return $ipv4; +} + +###################################################################### +## get_ipv6 +###################################################################### +sub get_ipv6 { + my $usev6 = lc(shift); ## Method to obtain IP address + $usev6 = 'disabled' if ($usev6 eq 'no'); # backward compatibility + my $h = shift; ## Host/service making the request + + my $ipv6 = undef; ## Found IPv6 address + my $reply = ''; ## Text returned from various methods + my $url = ''; ## URL of website or firewall + my $skip = ''; ## Regex of pattern to skip before looking for IP + my $arg = opt($usev6, $h) // ''; ## Value assigned to the "usev6" method + + if ($usev6 eq 'ipv6' || $usev6 eq 'ip') { + ## Static IPv6 address is provided in "ipv6=
" + if ($usev6 eq 'ip') { + warning("'usev6=ip' is deprecated. Use 'usev6=ipv6'"); + $usev6 = 'ipv6'; + ## If there is a value for ipv6= use that, else use value for ip= + $arg = opt($usev6, $h) // $arg; + } + $ipv6 = $arg; + if (!is_ipv6($ipv6)) { + warning("'%s' is not a valid IPv6",$ipv6 // ''); + $ipv6 = undef; + } + $arg = 'ipv6'; # For debug message at end of function + + } elsif ($usev6 eq 'ifv6' || $usev6 eq 'if' ) { + ## Obtain IPv6 address from interface mamed in "ifv6=" + if ($usev6 eq 'if') { + warning("'usev6=if' is deprecated. Use 'usev6=ifv6'"); + $usev6 = 'ifv6'; + ## If there is a value for ifv6= use that, else use value for if= + $arg = opt($usev6, $h) // $arg; + } + warning("'if-skip' is deprecated and does nothing for IPv6") if (opt('verbose') && opt('if-skip', $h)); + $ipv6 = get_ip_from_interface($arg,6); + + } elsif ($usev6 eq 'cmdv6' || $usev6 eq 'cmd') { + ## Obtain IPv6 address by executing the command in "cmdv6=" + if ($usev6 eq 'cmd') { + warning("'usev6=cmd' is deprecated. Use 'usev6=cmdv6'"); + $usev6 = 'cmdv6'; + ## If there is a value for cmdv6= use that, else use value for cmd= + $arg = opt($usev6, $h) // $arg; + } + warning("'cmd-skip' is deprecated and does nothing for IPv6") if (opt('verbose') && opt('cmd-skip', $h)); + if ($arg) { + my $sys_cmd = quotemeta($arg); + $reply = qx{$sys_cmd}; + $reply = '' if $?; + } + + } elsif ($usev6 eq 'webv6' || $usev6 eq 'web') { + ## Obtain IPv6 address by accessing website at url in "webv6=" + if ($usev6 eq 'web') { + warning("'usev6=web' is deprecated. Use 'usev6=webv6'"); + $usev6 = 'webv6'; + ## If there is a value for webv6= use that, else use value for web= + $arg = opt($usev6, $h) // $arg; + } + warning("'web-skip' does nothing for IPv6. Use 'webv6-skip'") if (opt('web-skip', $h)); + $url = $arg; + $skip = opt('webv6-skip', $h) // ''; + if (exists $builtinweb{$url}) { + $skip = $builtinweb{$url}->{'skip'} unless $skip; + $url = $builtinweb{$url}->{'url'}; + $arg = $url; + } + if ($url) { + $reply = geturl( + proxy => opt('proxy'), + url => $url, + ipversion => 6, # when using a URL to find IPv6 address we should force use of IPv6 + ssl_validate => opt('ssl-validate', $h), + ) // ''; + } + + } elsif ($usev6 eq 'cisco' || $usev6 eq 'cisco-asa') { + warning("'usev6=cisco' and 'usev6=cisco-asa' are not implemented and do nothing"); + $reply = ''; + + } elsif ($usev6 eq 'disabled') { + ## This is a no-op... Do not get an IPv6 address for this host/service + warning("'usev6=no' is deprecated. Use 'usev6=disabled'") if ($usev6 eq 'no'); + $reply = ''; + + } else { + warning("'usev6=%s' is not implemented and does nothing", $usev6); + $reply = ''; + + } + + ## Set to loopback address if no text set yet + $reply = '::' if !defined($reply); + if (($skip // '') ne '') { + $skip =~ s/ /\\s/is; + $reply =~ s/^.*?${skip}//is; + } + ## If $ipv6 not set yet look for IPv6 address in the $reply text + $ipv6 //= extract_ipv6($reply); + ## Return undef for loopback address unless statically assigned by "ipv6=::" + $ipv6 = undef if (($usev6 ne 'ipv6') && (($ipv6 // '') eq '::')); + debug("get_ipv6: using (%s, %s) reports %s", $usev6, $arg, $ipv6 // ""); + return $ipv6; +} + ###################################################################### ## group_hosts_by ###################################################################### sub group_hosts_by { +##TODO - Update for wantipv4 and wantipv6 my ($hosts, $attributes) = @_; my %attrs = (map({ ($_ => 1) } @$attributes), 'wantip' => 1); my @attrs = sort(keys(%attrs)); @@ -2984,12 +3472,35 @@ EoEXAMPLE } ###################################################################### ## nic_updateable +## Returns true if we can go ahead and update the IP address at server ###################################################################### sub nic_updateable { my $host = shift; my $sub = shift; my $update = 0; my $ip = $config{$host}{'wantip'}; + my $ipv4 = $config{$host}{'wantipv4'}; + my $ipv6 = $config{$host}{'wantipv6'}; + my $use = opt('use', $host) // 'disabled'; + my $usev4 = opt('usev4', $host) // 'disabled'; + my $usev6 = opt('usev6', $host) // 'disabled'; + $use = 'disabled' if ($use eq 'no'); # backward compatibility + $usev6 = 'disabled' if ($usev6 eq 'no'); # backward compatibility + + # If we have a valid IP address and we have previously warned that it was invalid. + # reset the warning count back to zero. + if (($use ne 'disabled') && $ip && $warned_ip{$host}) { + $warned_ip{$host} = 0; + warning("IP address for %s valid: %s. Reset warning count", $host, $ip); + } + if (($usev4 ne 'disabled') && $ipv4 && $warned_ipv4{$host}) { + $warned_ipv4{$host} = 0; + warning("IPv4 address for %s valid: %s. Reset warning count", $host, $ipv4); + } + if (($usev6 ne 'disabled') && $ipv6 && $warned_ipv6{$host}) { + $warned_ipv6{$host} = 0; + warning("IPv6 address for %s valid: %s. Reset warning count", $host, $ipv6); + } if ($config{$host}{'login'} eq '') { warning("null login name specified for host %s.", $host); @@ -3021,7 +3532,9 @@ sub nic_updateable { ); $update = 1; - } elsif (!exists($cache{$host}{'ip'}) || $cache{$host}{'ip'} ne $ip) { + } elsif ( ($use ne 'disabled') + && ((!exists($cache{$host}{'ip'})) || ("$cache{$host}{'ip'}" ne "$ip"))) { + ## Check whether to update IP address for the "use" method" if (($cache{$host}{'status'} eq 'good') && !interval_expired($host, 'mtime', 'min-interval')) { @@ -3036,17 +3549,117 @@ sub nic_updateable { $cache{$host}{'warned-min-interval'} = $now; - } elsif (($cache{$host}{'status'} ne 'good') && !interval_expired($host, 'atime', 'min-error-interval')) { + } elsif (($cache{$host}{'status'} ne 'good') && + !interval_expired($host, 'atime', 'min-error-interval')) { - warning("skipping update of %s from %s to %s.\nlast updated %s but last attempt on %s failed.\nWait at least %s between update attempts.", + if ( opt('verbose') + || ( ! $cache{$host}{'warned-min-error-interval'} + && (($warned_ip{$host} // 0) < $inv_ip_warn_count)) ) { + + warning("skipping update of %s from %s to %s.\nlast updated %s but last attempt on %s failed.\nWait at least %s between update attempts.", + $host, + ($cache{$host}{'ip'} ? $cache{$host}{'ip'} : ''), + $ip, + ($cache{$host}{'mtime'} ? prettytime($cache{$host}{'mtime'}) : ''), + ($cache{$host}{'atime'} ? prettytime($cache{$host}{'atime'}) : ''), + prettyinterval($config{$host}{'min-error-interval'}) + ); + if (!$ip && !opt('verbose')) { + $warned_ip{$host} = ($warned_ip{$host} // 0) + 1; + warning("IP address for %s undefined. Warned %s times, suppressing further warnings", $host, $inv_ip_warn_count) + if ($warned_ip{$host} >= $inv_ip_warn_count); + } + } + + $cache{$host}{'warned-min-error-interval'} = $now; + + } else { + $update = 1; + } + + } elsif ( ($usev4 ne 'disabled') + && ((!exists($cache{$host}{'ipv4'})) || ("$cache{$host}{'ipv4'}" ne "$ipv4"))) { + ## Check whether to update IPv4 address for the "usev4" method" + if (($cache{$host}{'status-ipv4'} eq 'good') && + !interval_expired($host, 'mtime', 'min-interval')) { + + warning("skipping update of %s from %s to %s.\nlast updated %s.\nWait at least %s between update attempts.", $host, - ($cache{$host}{'ip'} ? $cache{$host}{'ip'} : ''), - $ip, + ($cache{$host}{'ipv4'} ? $cache{$host}{'ipv4'} : ''), + $ipv4, ($cache{$host}{'mtime'} ? prettytime($cache{$host}{'mtime'}) : ''), - ($cache{$host}{'atime'} ? prettytime($cache{$host}{'atime'}) : ''), - prettyinterval($config{$host}{'min-error-interval'}) + prettyinterval($config{$host}{'min-interval'}) ) - if opt('verbose') || !($cache{$host}{'warned-min-error-interval'} // 0); + if opt('verbose') || !($cache{$host}{'warned-min-interval'} // 0); + + $cache{$host}{'warned-min-interval'} = $now; + + } elsif (($cache{$host}{'status-ipv4'} ne 'good') && + !interval_expired($host, 'atime', 'min-error-interval')) { + + if ( opt('verbose') + || ( ! $cache{$host}{'warned-min-error-interval'} + && (($warned_ipv4{$host} // 0) < $inv_ip_warn_count)) ) { + + warning("skipping update of %s from %s to %s.\nlast updated %s but last attempt on %s failed.\nWait at least %s between update attempts.", + $host, + ($cache{$host}{'ipv4'} ? $cache{$host}{'ipv4'} : ''), + $ipv4, + ($cache{$host}{'mtime'} ? prettytime($cache{$host}{'mtime'}) : ''), + ($cache{$host}{'atime'} ? prettytime($cache{$host}{'atime'}) : ''), + prettyinterval($config{$host}{'min-error-interval'}) + ); + if (!$ipv4 && !opt('verbose')) { + $warned_ipv4{$host} = ($warned_ipv4{$host} // 0) + 1; + warning("IPv4 address for %s undefined. Warned %s times, suppressing further warnings", $host, $inv_ip_warn_count) + if ($warned_ipv4{$host} >= $inv_ip_warn_count); + } + } + + $cache{$host}{'warned-min-error-interval'} = $now; + + } else { + $update = 1; + } + + } elsif ( ($usev6 ne 'disabled') + && ((!exists($cache{$host}{'ipv6'})) || ("$cache{$host}{'ipv6'}" ne "$ipv6"))) { + ## Check whether to update IPv6 address for the "usev6" method" + if (($cache{$host}{'status-ipv6'} eq 'good') && + !interval_expired($host, 'mtime', 'min-interval')) { + + warning("skipping update of %s from %s to %s.\nlast updated %s.\nWait at least %s between update attempts.", + $host, + ($cache{$host}{'ipv6'} ? $cache{$host}{'ipv6'} : ''), + $ipv6, + ($cache{$host}{'mtime'} ? prettytime($cache{$host}{'mtime'}) : ''), + prettyinterval($config{$host}{'min-interval'}) + ) + if opt('verbose') || !($cache{$host}{'warned-min-interval'} // 0); + + $cache{$host}{'warned-min-interval'} = $now; + + } elsif (($cache{$host}{'status-ipv6'} ne 'good') && + !interval_expired($host, 'atime', 'min-error-interval')) { + + if ( opt('verbose') + || ( ! $cache{$host}{'warned-min-error-interval'} + && (($warned_ipv6{$host} // 0) < $inv_ip_warn_count)) ) { + + warning("skipping update of %s from %s to %s.\nlast updated %s but last attempt on %s failed.\nWait at least %s between update attempts.", + $host, + ($cache{$host}{'ipv6'} ? $cache{$host}{'ipv6'} : ''), + $ipv6, + ($cache{$host}{'mtime'} ? prettytime($cache{$host}{'mtime'}) : ''), + ($cache{$host}{'atime'} ? prettytime($cache{$host}{'atime'}) : ''), + prettyinterval($config{$host}{'min-error-interval'}) + ); + if (!$ipv6 && !opt('verbose')) { + $warned_ipv6{$host} = ($warned_ipv6{$host} // 0) + 1; + warning("IPv6 address for %s undefined. Warned %s times, suppressing further warnings", $host, $inv_ip_warn_count) + if ($warned_ipv6{$host} >= $inv_ip_warn_count); + } + } $cache{$host}{'warned-min-error-interval'} = $now; @@ -3068,13 +3681,27 @@ sub nic_updateable { $update = 1; } else { - success("%s: skipped: IP address was already set to %s.", $host, $ip) - if opt('verbose'); + if (opt('verbose')) { + if ($use ne 'disabled') { + success("%s: skipped: IP address was already set to %s.", $host, $ip); + } + if ($usev4 ne 'disabled') { + success("%s: skipped: IPv4 address was already set to %s.", $host, $ipv6); + } + if ($usev6 ne 'disabled') { + success("%s: skipped: IPv6 address was already set to %s.", $host, $ipv6); + } + } } + $config{$host}{'status'} = $cache{$host}{'status'} // ''; + $config{$host}{'status-ipv4'} = $cache{$host}{'status-ipv4'} // ''; + $config{$host}{'status-ipv6'} = $cache{$host}{'status-ipv6'} // ''; $config{$host}{'update'} = $update; if ($update) { $config{$host}{'status'} = 'noconnect'; + $config{$host}{'status-ipv4'} = 'noconnect'; + $config{$host}{'status-ipv6'} = 'noconnect'; $config{$host}{'atime'} = $now; $config{$host}{'wtime'} = 0; $config{$host}{'warned-min-interval'} = 0; @@ -4333,12 +4960,14 @@ EoEXAMPLE ## hostname2.example.com|5.6.7.8|http://example/update/url3 ## hostname2.example.com|9.10.11.12|http://example/update/url4 ## hostname3.example.com|cafe::f00d|http://example/update/url5 +## hostname4.example.com|NULL|http://example/update/url6 ## ## The record's columns are separated by '|'. The first is the hostname, the second is the current ## address, and the third is the record-specific update URL. There can be multiple records for the -## same host, and they can even have the same address type. Any record can be updated to hold -## either type of address (e.g., if given an IPv6 address the record will automatically become an -## AAAA record). +## same host, and they can even have the same address type. To update an IP address the record +## must already exist of the type we want to update... We will not change a record type from +## an IPv4 to IPv6 or viz versa. Records may exist with a NULL address which we will allow to be +## updated with an IPv4 address, not an IPv6. ## ## The second step is to visit the appropriate record's update URL with ## ?address= appended. "Updated" in the result means success, "fail" means @@ -4354,7 +4983,10 @@ sub nic_freedns_update { my $url_tmpl = "http://$config{$_[0]}{'server'}/api/?action=getdyndns&v=2&sha="; my $creds = sha1_hex("$config{$_[0]}{'login'}|$config{$_[0]}{'password'}"); (my $url = $url_tmpl) =~ s//$creds/; - my $reply = geturl(proxy => opt('proxy'), url => $url); + + my $reply = geturl(proxy => opt('proxy'), + url => $url + ); my $record_list_error = ''; if ($reply && header_ok($_[0], $reply)) { $reply =~ s/^.*?\n\n//s; # Strip the headers. @@ -4363,7 +4995,8 @@ sub nic_freedns_update { next if ($#rec < 2); my $recs = is_ipv6($rec[1]) ? \%recs_ipv6 : \%recs_ipv4; $recs->{$rec[0]} = \@rec; - debug("host: %s, current address: %s, update URL: %s", @rec); + # Update URL contains credentials that don't require login to use, so best to hide. + debug("host: %s, current address: %s, update URL: ", $rec[0], $rec[1]); } if (keys(%recs_ipv4) + keys(%recs_ipv6) == 0) { chomp($reply); @@ -4374,54 +5007,61 @@ sub nic_freedns_update { } foreach my $h (@_) { - if (!$h) { next } - my $ip = delete $config{$h}{'wantip'}; - - info("%s: setting IP address to %s", $h, $ip); + next if (!$h); + my $ipv4 = delete $config{$h}{'wantipv4'}; + my $ipv6 = delete $config{$h}{'wantipv6'}; if ($record_list_error ne '') { - $config{$h}{'status'} = 'failed'; + $config{$h}{'status-ipv4'} = 'failed' if ($ipv4); + $config{$h}{'status-ipv6'} = 'failed' if ($ipv6); failed("updating %s: %s", $h, $record_list_error); next; } - # If there is a record with matching type then update it, otherwise let - # freedns convert the record to the desired type. - my $rec = is_ipv6($ip) - ? ($recs_ipv6{$h} // $recs_ipv4{$h}) - : ($recs_ipv4{$h} // $recs_ipv6{$h}); - if (!defined($rec)) { - $config{$h}{'status'} = 'failed'; - failed("updating %s: host record does not exist", $h); - next; - } - if ($ip eq $rec->[1]) { - $config{$h}{'ip'} = $ip; - $config{$h}{'mtime'} = $now; - $config{$h}{'status'} = 'good'; - success("update not necessary %s: good: IP address already set to %s", $h, $ip) - if (!$daemon || opt('verbose')); - } else { - my $url = $rec->[2] . "&address=" . $ip; - debug("Update: %s", $url); - my $reply = geturl(proxy => opt('proxy'), url => $url); - if (!defined($reply) || !$reply || !header_ok($h, $reply)) { - $config{$h}{'status'} = 'failed'; - failed("updating %s: Could not connect to %s.", $h, $url); + # IPv4 and IPv6 handling are similar enough to do in a loop... + foreach my $ip ($ipv4, $ipv6) { + next if (!$ip); + my $ipv = ($ip eq ($ipv6 // '')) ? '6' : '4'; + my $type = ($ip eq ($ipv6 // '')) ? 'AAAA' : 'A'; + my $rec = ($ip eq ($ipv6 // '')) ? $recs_ipv6{$h} + : $recs_ipv4{$h}; + if (!$rec) { + failed("updating %s: Cannot set IPv$ipv to %s No '$type' record at FreeDNS", $h, $ip); next; } - $reply =~ s/^.*?\n\n//s; # Strip the headers. - if ($reply =~ /Updated.*$h.*to.*$ip/) { - $config{$h}{'ip'} = $ip; - $config{$h}{'mtime'} = $now; - $config{$h}{'status'} = 'good'; - success("updating %s: good: IP address set to %s", $h, $ip); + info("updating %s: setting IP address to %s", $h, $ip); + $config{$h}{"status-ipv$ipv"} = 'failed'; + + if ($ip eq $rec->[1]) { + $config{$h}{"ipv$ipv"} = $ip; + $config{$h}{'mtime'} = $now; + $config{$h}{"status-ipv$ipv"} = 'good'; + success("updating %s: update not necessary, '$type' record already set to %s", $h, $ip) + if (!$daemon || opt('verbose')); } else { - $config{$h}{'status'} = 'failed'; - warning("SENT: %s", $url) unless opt('verbose'); - warning("REPLIED: %s", $reply); - failed("updating %s: Invalid reply.", $h); + my $url = $rec->[2] . "&address=" . $ip; + ($url_tmpl = $url) =~ s/\?.*\&/?&/; # redact unique update token + debug("updating: %s", $url_tmpl); + + my $reply = geturl(proxy => opt('proxy'), + url => $url + ); + if ($reply && header_ok($h, $reply)) { + $reply =~ s/^.*?\n\n//s; # Strip the headers. + if ($reply =~ /Updated.*$h.*to.*$ip/) { + $config{$h}{"ipv$ipv"} = $ip; + $config{$h}{'mtime'} = $now; + $config{$h}{"status-ipv$ipv"} = 'good'; + success("updating %s: good: IPv$ipv address set to %s", $h, $ip); + } else { + warning("SENT: %s", $url_tmpl) unless opt('verbose'); + warning("REPLIED: %s", $reply); + failed("updating %s: Invalid reply.", $h); + } + } else { + failed("updating %s: Could not connect to %s.", $h, $url_tmpl); + } } } } @@ -4759,7 +5399,6 @@ sub nic_cloudflare_update { my @hosts = @{$groups{$sig}}; my $hosts = join(',', @hosts); my $key = $hosts[0]; - my $ip = $config{$key}{'wantip'}; my $headers = "Content-Type: application/json\n"; if ($config{$key}{'login'} eq 'token') { @@ -4772,102 +5411,104 @@ sub nic_cloudflare_update { # FQDNs for my $domain (@hosts) { (my $hostname = $domain) =~ s/\.$config{$key}{zone}$//; - delete $config{$domain}{'wantip'}; + my $ipv4 = delete $config{$domain}{'wantipv4'}; + my $ipv6 = delete $config{$domain}{'wantipv6'}; - info("setting IP address to %s for %s", $ip, $domain); - verbose("UPDATE:", "updating %s", $domain); + info("getting Cloudflare Zone ID for %s", $domain); # Get zone ID my $url = "https://$config{$key}{'server'}/zones?"; - $url .= "name=".$config{$key}{'zone'}; + $url .= "name=" . $config{$key}{'zone'}; - my $reply = geturl(proxy => opt('proxy'), url => $url, headers => $headers); - unless ($reply) { + my $reply = geturl(proxy => opt('proxy'), + url => $url, + headers => $headers + ); + unless ($reply && header_ok($domain, $reply)) { failed("updating %s: Could not connect to %s.", $domain, $config{$key}{'server'}); next; } - next if !header_ok($domain, $reply); # Strip header $reply =~ s/^.*?\n\n//s; - my $response = eval { decode_json($reply) }; - if (!defined $response || !defined $response->{result}) { - failed("invalid json or result."); + my $response = eval {decode_json($reply)}; + unless ($response && $response->{result}) { + failed("updating %s: invalid json or result.", $domain); next; } # Pull the ID out of the json, messy - my ($zone_id) = map { $_->{name} eq $config{$key}{'zone'} ? $_->{id} : () } @{$response->{result}}; + my ($zone_id) = map {$_->{name} eq $config{$key}{'zone'} ? $_->{id} : ()} @{$response->{result}}; unless ($zone_id) { failed("updating %s: No zone ID found.", $config{$key}{'zone'}); next; } - info("zone ID is %s", $zone_id); + info("Zone ID is %s", $zone_id); - # Get DNS record ID - $url = "https://$config{$key}{'server'}/zones/$zone_id/dns_records?"; - if (is_ipv6($ip)) { - $url .= "type=AAAA&name=$domain"; - } else { - $url .= "type=A&name=$domain"; + + # IPv4 and IPv6 handling are similar enough to do in a loop... + foreach my $ip ($ipv4, $ipv6) { + next if (!$ip); + my $ipv = ($ip eq ($ipv6 // '')) ? '6' : '4'; + my $type = ($ip eq ($ipv6 // '')) ? 'AAAA' : 'A'; + + info("updating %s: setting IPv$ipv address to %s", $domain, $ip); + $config{$domain}{"status-ipv$ipv"} = 'failed'; + + # Get DNS 'A' or 'AAAA' record ID + $url = "https://$config{$key}{'server'}/zones/$zone_id/dns_records?"; + $url .= "type=$type&name=$domain"; + $reply = geturl(proxy => opt('proxy'), + url => $url, + headers => $headers + ); + unless ($reply && header_ok($domain, $reply)) { + failed("updating %s: Could not connect to %s.", $domain, $config{$key}{'server'}); + next; + } + # Strip header + $reply =~ s/^.*?\n\n//s; + $response = eval {decode_json($reply)}; + unless ($response && $response->{result}) { + failed("updating %s: invalid json or result.", $domain); + next; + } + # Pull the ID out of the json, messy + my ($dns_rec_id) = map {$_->{name} eq $domain ? $_->{id} : ()} @{$response->{result}}; + unless($dns_rec_id) { + failed("updating %s: Cannot set IPv$ipv to %s No '$type' record at Cloudflare", $domain, $ip); + next; + } + debug("updating %s: DNS '$type' record ID: $dns_rec_id", $domain); + # Set domain + $url = "https://$config{$key}{'server'}/zones/$zone_id/dns_records/$dns_rec_id"; + my $data = "{\"content\":\"$ip\"}"; + $reply = geturl(proxy => opt('proxy'), + url => $url, + headers => $headers, + method => "PATCH", + data => $data + ); + unless ($reply && header_ok($domain, $reply)) { + failed("updating %s: Could not connect to %s.", $domain, $config{$domain}{'server'}); + next; + } + # Strip header + $reply =~ s/^.*?\n\n//s; + $response = eval {decode_json($reply)}; + if ($response && $response->{result}) { + success("updating %s: IPv$ipv address set to %s", $domain, $ip); + $config{$domain}{"ipv$ipv"} = $ip; + $config{$domain}{'mtime'} = $now; + $config{$domain}{"status-ipv$ipv"} = 'good'; + } else { + failed("updating %s: invalid json or result.", $domain); + } } - - $reply = geturl(proxy => opt('proxy'), url => $url, headers => $headers); - unless ($reply) { - failed("updating %s: Could not connect to %s.", $domain, $config{$key}{'server'}); - next; - } - next if !header_ok($domain, $reply); - - # Strip header - $reply =~ s/^.*?\n\n//s; - $response = eval { decode_json($reply) }; - if (!defined $response || !defined $response->{result}) { - failed("invalid json or result."); - next; - } - - # Pull the ID out of the json, messy - my ($dns_rec_id) = map { $_->{name} eq $domain ? $_->{id} : () } @{$response->{result}}; - unless ($dns_rec_id) { - failed("updating %s: No DNS record ID found.", $domain); - next; - } - info("DNS record ID is %s", $dns_rec_id); - - # Set domain - $url = "https://$config{$key}{'server'}/zones/$zone_id/dns_records/$dns_rec_id"; - my $data = "{\"content\":\"$ip\"}"; - $reply = geturl( - proxy => opt('proxy'), - url => $url, - headers => $headers, - method => "PATCH", - data => $data, - ); - unless ($reply) { - failed("updating %s: Could not connect to %s.", $domain, $config{$domain}{'server'}); - next; - } - next if !header_ok($domain, $reply); - - # Strip header - $reply =~ s/^.*?\n\n//s; - $response = eval { decode_json($reply) }; - if (!defined $response || !defined $response->{result}) { - failed("invalid json or result."); - } else { - success("%s -- Updated Successfully to %s", $domain, $ip); - - } - - # Cache - $config{$domain}{'ip'} = $ip; - $config{$domain}{'mtime'} = $now; - $config{$domain}{'status'} = 'good'; } } } + ###################################################################### ## nic_yandex_examples ###################################################################### From 7fea824ec165087205b46ae7fd5b39cf6b056cbe Mon Sep 17 00:00:00 2001 From: James Davidson Date: Tue, 11 May 2021 18:19:37 -0700 Subject: [PATCH 16/16] Fix sample duckdns.org config (#301) The included configuration for duckdns.org is incorrect and does not match the protocol sample in the wiki[1]. [1] https://sourceforge.net/p/ddclient/wiki/protocols/#duckdns --- ddclient.conf.in | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ddclient.conf.in b/ddclient.conf.in index b36b514..33ab408 100644 --- a/ddclient.conf.in +++ b/ddclient.conf.in @@ -218,8 +218,9 @@ ssl=yes # use ssl-support. Works with ## Duckdns (http://www.duckdns.org/) ## # -# password=my-auto-generated-password -# protocol=duckdns hostwithoutduckdnsorg +# protocol=duckdns, \ +# password=my-auto-generated-password \ +# hostwithoutduckdnsorg ## ## Freemyip (http://freemyip.com/)