diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4ad62c1..ebe4d0d 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
@@ -52,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
@@ -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/.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..ce01304 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,7 @@ AM_PL_LOG_FLAGS = -Mstrict -w \
-I'$(abs_top_srcdir)'/t/lib \
-MDevel::Autoflush
handwritten_tests = \
- t/geturl_connectivity.pl \
+ t/get_ip_from_if.pl \
t/geturl_ssl.pl \
t/is-and-extract-ipv4.pl \
t/is-and-extract-ipv6.pl \
@@ -78,6 +79,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.conf.in b/ddclient.conf.in
index 164a260..33ab408 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
@@ -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/)
diff --git a/ddclient.in b/ddclient.in
index cef45e1..c47a538 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);
@@ -74,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' }
@@ -89,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.
@@ -351,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)),
);
@@ -368,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,
@@ -388,27 +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),
- 'ipv6' => setv(T_BOOL, 0, 0, 0, undef),
+ 'curl' => 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),
@@ -432,18 +493,23 @@ 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),
- '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),
@@ -780,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 "],
@@ -812,19 +896,22 @@ 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"],
["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"],
@@ -900,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');
@@ -961,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'};
@@ -971,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) {
@@ -1045,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);
}
@@ -1334,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
@@ -1525,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');
@@ -1568,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');
}
######################################################################
@@ -1940,6 +2184,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 "";
@@ -1954,6 +2206,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;
}
@@ -2042,10 +2301,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};
@@ -2098,7 +2362,7 @@ sub geturl {
} 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)) {
@@ -2133,6 +2397,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';
@@ -2213,11 +2480,232 @@ 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 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
+ $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);
+ (my $_url = $url) =~ s%\?.*%?%; #redact possible credentials
+ debug("url = %s", $_url);
+ debug("ip ver = %s", $ipversion);
+
+ if (!opt('exec')) {
+ debug("skipped network connection");
+ verbose("SENDING:", "%s", "${server}/${url}");
+ } else {
+ 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;
+
+ $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);
+
+ $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}");
+
+ # 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});
+ $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}");
+ verbose("SENDING:", "%s", $headers) if ($headers);
+ verbose("SENDING:", "%s", $data) if ($data);
+
+ 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);
+
+ # 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);
+ }
+ }
+
+ ## 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
######################################################################
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;
@@ -2225,15 +2713,13 @@ 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';
} 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) {
@@ -2253,7 +2739,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')) {
@@ -2275,6 +2765,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;
@@ -2297,9 +2788,14 @@ 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;
+ } 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) // '';
@@ -2316,6 +2812,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),
) // '';
}
}
@@ -2336,7 +2833,6 @@ sub get_ip {
return $ip;
}
-
######################################################################
## Regex to find IPv4 address. Accepts embedded leading zeros.
######################################################################
@@ -2427,6 +2923,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
@@ -2459,10 +2959,417 @@ 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_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 // "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
+ 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 ((lc($interface) eq "default") && (!$reply)) { ## skip if test data passed in.
+ $interface = get_default_interface($ipver);
+ 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]);
+}
+
+######################################################################
+## 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));
@@ -2570,12 +3477,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);
@@ -2607,7 +3537,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')) {
@@ -2622,17 +3554,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;
@@ -2654,13 +3686,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;
@@ -2680,7 +3726,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$/) {
@@ -3919,12 +4965,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
@@ -3940,7 +4988,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.
@@ -3949,7 +5000,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);
@@ -3960,54 +5012,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);
+ }
}
}
}
@@ -4319,6 +5378,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
@@ -4344,7 +5404,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') {
@@ -4357,102 +5416,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
######################################################################
@@ -5329,13 +6390,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;
diff --git a/t/get_ip_from_if.pl b/t/get_ip_from_if.pl
new file mode 100644
index 0000000..6f08e5d
--- /dev/null
+++ b/t/get_ip_from_if.pl
@@ -0,0 +1,63 @@
+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_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
+ 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});
+ }
+ }
+};
+
+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/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 => '/',
},
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..c546b9c 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,495 @@ 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",
+ },
+);
+
+######################################################################
+## 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",
},
);