diff --git a/ddclient.in b/ddclient.in index 445979f..8344e44 100755 --- a/ddclient.in +++ b/ddclient.in @@ -75,6 +75,9 @@ my ($result, %config, %cache); my $saved_cache; my %saved_opt; my $daemon; +# Control how many times warning message logged for invalid IP addresses +my (%warned_ip, %warned_ipv4, %warned_ipv6); +my $inv_ip_warn_count = opt('max-warn') // 1; sub T_ANY { 'any' } sub T_STRING { 'string' } @@ -90,9 +93,13 @@ sub T_FILE { 'file name' } sub T_FQDNP { 'fully qualified host name and optional port number' } sub T_PROTO { 'protocol' } sub T_USE { 'ip strategy' } +sub T_USEV4 { 'ipv4 strategy' } +sub T_USEV6 { 'ipv6 strategy' } sub T_IF { 'interface' } sub T_PROG { 'program name' } sub T_IP { 'ip' } +sub T_IPV4 { 'ipv4' } +sub T_IPV6 { 'ipv6' } sub T_POSTS { 'postscript' } ## strategies for obtaining an ip address. @@ -352,14 +359,15 @@ my %builtinfw = ( ); my %ip_strategies = ( - 'ip' => ": use IP given by '-ip
'", - 'web' => ": obtain IP from the web-based IP discovery service given by '-web |'", - 'fw' => ": obtain IP from a firewall/router device by visiting the URL given by '-fw '", - 'if' => ": obtain IP from the interface given by '-if '", - 'cmd' => ": obtain IP by running the command given by '-cmd '", - 'cisco' => ": obtain IP from Cisco FW device at the address given by '-fw
'", - 'cisco-asa' => ": obtain IP from Cisco ASA device at the address given by '-fw
'", - map({ $_ => sprintf(": obtain IP from %s device at the address given by '-fw
'", + 'no' => ": deprecated, see 'usev4' and 'usev6'", + 'ip' => ": deprecated, see 'usev4' and 'usev6'", + 'web' => ": deprecated, see 'usev4' and 'usev6'", + 'fw' => ": deprecated, see 'usev4' and 'usev6'", + 'if' => ": deprecated, see 'usev4' and 'usev6'", + 'cmd' => ": deprecated, see 'usev4' and 'usev6'", + 'cisco' => ": deprecated, see 'usev4' and 'usev6'", + 'cisco-asa' => ": deprecated, see 'usev4' and 'usev6'", + map({ $_ => sprintf(": Built-in firewall %s deprecated, see 'usev4' and 'usev6'", $builtinfw{$_}->{'name'}) } keys(%builtinfw)), ); @@ -369,6 +377,41 @@ sub ip_strategies_usage { ('ip', 'web', 'if', 'cmd', 'fw', sort('cisco', 'cisco-asa', keys(%builtinfw)))); } +my %ipv4_strategies = ( + 'disabled' => ": do not obtain an IPv4 address for this host", + 'ipv4' => ": obtain IPv4 from -ipv4 {address}", + 'webv4' => ": obtain IPv4 from an IP discovery page on the web", + 'ifv4' => ": obtain IPv4 from the -ifv4 {interface}", + 'cmdv4' => ": obtain IPv4 from the -cmdv4 {external-command}", + 'fwv4' => ": obtain IPv4 from the firewall specified by -fwv4 {type|address}", + 'ciscov4' => ": obtain IPv4 from Cisco FW at the -fwv4 {address}", + 'cisco-asav4' => ": obtain IPv4 from Cisco ASA at the -fwv4 {address}", + map { $_ => sprintf ": obtain IPv4 from %s at the -fwv4 {address}", $builtinfw{$_}->{'name'} } keys %builtinfw, +); +sub ipv4_strategies_usage { + return map { sprintf(" -usev4=%-22s %s.", $_, $ipv4_strategies{$_}) } sort keys %ipv4_strategies; +} + +my %ipv6_strategies = ( + 'no' => ": deprecated, use 'disabled'", + 'disabled' => ": do not obtain an IPv6 address for this host", + 'ip' => ": deprecated, use 'ipv6'", + 'ipv6' => ": obtain IPv6 from -ipv6 {address}", + 'web' => ": deprecated, use 'webv6'", + 'webv6' => ": obtain IPv6 from an IP discovery page on the web", + 'if' => ": deprecated, use 'ifv6'", + 'ifv6' => ": obtain IPv6 from the -if {interface}", + 'cmd' => ": deprecated, use 'cmdv6'", + 'cmdv6' => ": obtain IPv6 from the -cmdv6 {external-command}", + 'fwv6' => ": obtain IPv6 from the firewall specified by -fwv6 {type|address}", + 'ciscov6' => ": obtain IPv6 from Cisco FW at the -fwv6 {address}", + 'cisco-asav6' => ": obtain IPv6 from Cisco ASA at the -fwv6 {address}", + map { $_ => sprintf ": obtain IPv6 from %s at the -fwv6 {address}", $builtinfw{$_}->{'name'} } keys %builtinfw, +); +sub ipv6_strategies_usage { + return map { sprintf(" -usev6=%-22s %s.", $_, $ipv6_strategies{$_}) } sort keys %ipv6_strategies; +} + sub setv { return { 'type' => shift, @@ -389,28 +432,44 @@ my %variables = ( 'protocol' => setv(T_PROTO, 0, 0, 'dyndns2', undef), 'use' => setv(T_USE, 0, 0, 'ip', undef), + 'usev4' => setv(T_USEV4, 0, 0, 'disabled', undef), + 'usev6' => setv(T_USEV6, 0, 0, 'disabled', undef), 'ip' => setv(T_IP, 0, 0, undef, undef), + 'ipv4' => setv(T_IPV4, 0, 0, undef, undef), + 'ipv6' => setv(T_IPV6, 0, 0, undef, undef), 'if' => setv(T_IF, 0, 0, 'ppp0', undef), + 'ifv4' => setv(T_IF, 0, 0, 'default', undef), + 'ifv6' => setv(T_IF, 0, 0, 'default', undef), 'web' => setv(T_STRING,0, 0, 'dyndns', undef), 'web-skip' => setv(T_STRING,1, 0, '', undef), + 'webv4' => setv(T_STRING,0, 0, 'googledomains', undef), + 'webv4-skip' => setv(T_STRING,1, 0, '', undef), + 'webv6' => setv(T_STRING,0, 0, 'googledomains', undef), + 'webv6-skip' => setv(T_STRING,1, 0, '', undef), 'fw' => setv(T_ANY, 0, 0, '', undef), 'fw-skip' => setv(T_STRING,1, 0, '', undef), + 'fwv4' => setv(T_ANY, 0, 0, '', undef), + 'fwv4-skip' => setv(T_STRING,1, 0, '', undef), + 'fwv6' => setv(T_ANY, 0, 0, '', undef), + 'fwv6-skip' => setv(T_STRING,1, 0, '', undef), 'fw-login' => setv(T_LOGIN, 1, 0, '', undef), 'fw-password' => setv(T_PASSWD,1, 0, '', undef), 'cmd' => setv(T_PROG, 0, 0, '', undef), 'cmd-skip' => setv(T_STRING,1, 0, '', undef), + 'cmdv4' => setv(T_PROG, 0, 0, '', undef), + 'cmdv6' => setv(T_PROG, 0, 0, '', undef), 'timeout' => setv(T_DELAY, 0, 0, interval('120s'), interval('120s')), 'retry' => setv(T_BOOL, 0, 0, 0, undef), 'force' => setv(T_BOOL, 0, 0, 0, undef), 'ssl' => setv(T_BOOL, 0, 0, 0, undef), 'curl' => setv(T_BOOL, 0, 0, 0, undef), - 'ipv6' => setv(T_BOOL, 0, 0, 0, undef), 'syslog' => setv(T_BOOL, 0, 0, 0, undef), 'facility' => setv(T_STRING,0, 0, 'daemon', undef), 'priority' => setv(T_STRING,0, 0, 'notice', undef), 'mail' => setv(T_EMAIL, 0, 0, '', undef), 'mail-failure' => setv(T_EMAIL, 0, 0, '', undef), + 'max-warn' => setv(T_NUMBER,0, 0, 1, undef), 'exec' => setv(T_BOOL, 0, 0, 1, undef), 'debug' => setv(T_BOOL, 0, 0, 0, undef), @@ -442,12 +501,15 @@ my %variables = ( 'fw-ssl-validate' => setv(T_BOOL, 0, 0, 1, undef), 'cmd' => setv(T_PROG, 0, 0, '', undef), 'cmd-skip' => setv(T_STRING,0, 0, '', undef), - 'ipv6' => setv(T_BOOL, 0, 0, 0, undef), - 'ip' => setv(T_IP, 0, 1, undef, undef), + 'ip' => setv(T_IP, 0, 1, undef, undef), #TODO remove from cache? + 'ipv4' => setv(T_IPV4, 0, 1, undef, undef), + 'ipv6' => setv(T_IPV6, 0, 1, undef, undef), 'wtime' => setv(T_DELAY, 0, 1, 0, interval('30s')), 'mtime' => setv(T_NUMBER,0, 1, 0, undef), 'atime' => setv(T_NUMBER,0, 1, 0, undef), - 'status' => setv(T_ANY, 0, 1, '', undef), + 'status' => setv(T_ANY, 0, 1, '', undef), #TODO remove from cache? + 'status-ipv4' => setv(T_ANY, 0, 1, '', undef), + 'status-ipv6' => setv(T_ANY, 0, 1, '', undef), 'min-interval' => setv(T_DELAY, 0, 0, interval('30s'), 0), 'max-interval' => setv(T_DELAY, 0, 0, interval('25d'), 0), 'min-error-interval' => setv(T_DELAY, 0, 0, interval('5m'), 0), @@ -784,28 +846,46 @@ my @opt = ( ["cache", "=s", "-cache : record address used in "], ["pid", "=s", "-pid : record process id in if daemonized"], "", - ["use", "=s", "-use : how the IP address should be obtained"], + ["use", "=s", "-use : deprecated, see 'usev4' and 'usev6'"], &ip_strategies_usage(), + [ "usev4", "=s", "-usev4 : how the should IPv4 address be obtained."], + &ipv4_strategies_usage(), + [ "usev6", "=s", "-usev6 : how the should IPv6 address be obtained."], + &ipv6_strategies_usage(), "", " Options that apply to 'use=ip':", - ["ip", "=s", "-ip
: set the IP address to
"], + ["ip", "=s", "-ip
: deprecated, use 'ipv4' or 'ipv6'"], + ["ipv4", "=s", "-ipv4
: set the IPv4 address to
"], + ["ipv6", "=s", "-ipv6
: set the IPv6 address to
"], "", " Options that apply to 'use=if':", - ["if", "=s", "-if : obtain IP address from "], + ["if", "=s", "-if : deprecated, use 'ifv4' or 'ifv6'"], + ["ifv4", "=s", "-ifv4 : obtain IPv4 address from "], + ["ifv6", "=s", "-ifv6 : obtain IPv6 address from "], "", " Options that apply to 'use=web':", - ["web", "=s", "-web | : obtain IP address from a web-based IP discovery service, either a known or a custom "], - ["web-skip", "=s", "-web-skip : skip any IP addresses before in the text returned from the web-based IP discovery service"], + ["web", "=s", "-web | : deprecated, use 'webv4' or 'webv6'"], + ["web-skip", "=s", "-web-skip : deprecated, use 'webv4-skip' or 'webv6-skip'"], + ["webv4", "=s", "-webv4 |: obtain IPv4 address from a web-based IP discovery service, either a known or a custom "], + ["webv4-skip", "=s", "-webv4-skip : skip any IP addresses before in the output of 'ip address show dev ' (or 'ifconfig ')"], + ["webv6", "=s", "-webv6 |: obtain IPv6 address from a web-based IP discovery service, either a known or a custom "], + ["webv6-skip", "=s", "-webv6-skip : skip any IP addresses before in the output of 'ip address show dev ' (or 'ifconfig ')"], "", " Options that apply to 'use=fw' and 'use=':", - ["fw", "=s", "-fw
| : obtain IP address from device with IP address
or URL "], - ["fw-skip", "=s", "-fw-skip : skip any IP addresses before in the text returned from the device"], + ["fw", "=s", "-fw
| : deprecated, use 'fwv4' or 'fwv6'"], + ["fw-skip", "=s", "-fw-skip : deprecated, use 'fwv4-skip' or 'fwv6-skip'"], + ["fwv4", "=s", "-fwv4
| : obtain IPv4 address from device with IP address
or URL "], + ["fwv4-skip", "=s", "-fwv4-skip : skip any IP addresses before in the text returned from the device"], + ["fwv6", "=s", "-fwv6
| : obtain IPv6 address from device with IP address
or URL "], + ["fwv6-skip", "=s", "-fwv6-skip : skip any IP addresses before in the text returned from the device"], ["fw-login", "=s", "-fw-login : use when getting the IP from the device"], ["fw-password", "=s", "-fw-password : use password when getting the IP from the device"], "", " Options that apply to 'use=cmd':", - ["cmd", "=s", "-cmd : obtain IP address from the output of "], - ["cmd-skip", "=s", "-cmd-skip : skip any IP addresses before in the command's output"], + ["cmd", "=s", "-cmd : deprecated, use 'cmdv4' or 'cmdv6'"], + ["cmd-skip", "=s", "-cmd-skip : deprecated, filter in program wrapper script"], + ["cmdv4", "=s", "-cmdv4 : obtain IPv4 address from the output of "], + ["cmdv6", "=s", "-cmdv6 : obtain IPv6 address from the output of "], "", ["login", "=s", "-login : log in to the dynamic DNS service as "], ["password", "=s", "-password : log in to the dynamic DNS service with password "], @@ -825,13 +905,13 @@ my @opt = ( ["syslog", "!", "-{no}syslog : log messages to syslog"], ["facility", "=s", "-facility : log messages to syslog to facility "], ["priority", "=s", "-priority : log messages to syslog with priority "], + ["max-warn", "=i", "-max-warn : log at most warning messages for undefined IP address"], ["mail", "=s", "-mail
: e-mail messages to
"], ["mail-failure", "=s", "-mail-failure : e-mail messages for failed updates to "], ["exec", "!", "-{no}exec : do {not} execute; just show what would be done"], ["debug", "!", "-{no}debug : print {no} debugging information"], ["verbose", "!", "-{no}verbose : print {no} verbose information"], ["quiet", "!", "-{no}quiet : print {no} messages for unnecessary updates"], - ["ipv6", "!", "-{no}ipv6 : use ipv6"], ["help", "", "-help : display this message and exit"], ["postscript", "", "-postscript : script to run after updating ddclient, has new IP as param"], ["query", "!", "-{no}query : print {no} ip addresses and exit"], @@ -907,6 +987,10 @@ sub main { fatal("invalid argument '-use %s'; possible values are:\n%s", $opt{'use'}, join("\n", ip_strategies_usage())) unless exists $ip_strategies{lc opt('use')}; + if (defined($opt{'usev6'})) { + usage("invalid argument '-usev6 %s'; possible values are:\n%s", $opt{'usev6'}, join("\n",ipv6_strategies_usage())) + unless exists $ipv6_strategies{lc opt('usev6')}; + } $daemon = opt('daemon'); @@ -968,9 +1052,11 @@ sub runpostscript { sub update_nics { my %examined = (); my %iplist = (); + my %ipv4list = (); + my %ipv6list = (); foreach my $s (sort keys %services) { - my (@hosts, %ips) = (); + my (@hosts, %ipsv4, %ipsv6) = (); my $updateable = $services{$s}{'updateable'}; my $update = $services{$s}{'update'}; @@ -978,33 +1064,103 @@ sub update_nics { next if $config{$h}{'protocol'} ne lc($s); $examined{$h} = 1; # we only do this once per 'use' and argument combination - my $use = opt('use', $h); - my $arg_ip = opt('ip', $h) // ''; - my $arg_fw = opt('fw', $h) // ''; - my $arg_if = opt('if', $h) // ''; - my $arg_web = opt('web', $h) // ''; - my $arg_cmd = opt('cmd', $h) // ''; - my $ip = ""; - if (exists $iplist{$use}{$arg_ip}{$arg_fw}{$arg_if}{$arg_web}{$arg_cmd}) { - $ip = $iplist{$use}{$arg_ip}{$arg_fw}{$arg_if}{$arg_web}{$arg_cmd}; - } else { - $ip = get_ip($use, $h); - if (!defined($ip)) { - warning("unable to determine IP address") - if !$daemon || opt('verbose'); - next; + my $use = opt('use', $h) // 'disabled'; + my $usev4 = opt('usev4', $h) // 'disabled'; + my $usev6 = opt('usev6', $h) // 'disabled'; + $use = 'disabled' if ($use eq 'no'); # backward compatibility + $usev6 = 'disabled' if ($usev6 eq 'no'); # backward compatibility + my $arg_ip = opt('ip', $h) // ''; + my $arg_ipv4 = opt('ipv4', $h) // ''; + my $arg_ipv6 = opt('ipv6', $h) // ''; + my $arg_fw = opt('fw', $h) // ''; + my $arg_fwv4 = opt('fwv4', $h) // ''; + my $arg_fwv6 = opt('fwv6', $h) // ''; + my $arg_if = opt('if', $h) // ''; + my $arg_ifv4 = opt('ifv4', $h) // ''; + my $arg_ifv6 = opt('ifv6', $h) // ''; + my $arg_web = opt('web', $h) // ''; + my $arg_webv4 = opt('webv4', $h) // ''; + my $arg_webv6 = opt('webv6', $h) // ''; + my $arg_cmd = opt('cmd', $h) // ''; + my $arg_cmdv4 = opt('cmdv4', $h) // ''; + my $arg_cmdv6 = opt('cmdv6', $h) // ''; + my $ip = undef; + my $ipv4 = undef; + my $ipv6 = undef; + + if ($use ne 'disabled') { + if (exists $iplist{$use}{$arg_ip}{$arg_fw}{$arg_if}{$arg_web}{$arg_cmd}) { + # If we have already done a get_ip() for this, don't do it again. + $ip = $iplist{$use}{$arg_ip}{$arg_fw}{$arg_if}{$arg_web}{$arg_cmd}; + } else { + # Else need to find the IP address... + $ip = get_ip($use, $h); + if (is_ipv4($ip) || is_ipv6($ip)) { + # And if it is valid, remember it... + $iplist{$use}{$arg_ip}{$arg_fw}{$arg_if}{$arg_web}{$arg_cmd} = $ip; + } else { + warning("%s: unable to determine IP address with strategy use=%s", $h, $use) + if !$daemon || opt('verbose'); + } } - $iplist{$use}{$arg_ip}{$arg_fw}{$arg_if}{$arg_web}{$arg_cmd} = $ip; + # And remember it as the IP address we want to send to the DNS service. + $config{$h}{'wantip'} = $ip; } - $config{$h}{'wantip'} = $ip; + + if ($usev4 ne 'disabled') { + if (exists $ipv4list{$usev4}{$arg_ipv4}{$arg_fwv4}{$arg_ifv4}{$arg_webv4}{$arg_cmdv4}) { + # If we have already done a get_ipv4() for this, don't do it again. + $ipv4 = $ipv4list{$usev4}{$arg_ipv4}{$arg_fwv4}{$arg_ifv4}{$arg_webv4}{$arg_cmdv4}; + } else { + # Else need to find the IPv4 address... + $ipv4 = get_ipv4($usev4, $h); + if (is_ipv4($ipv4)) { + # And if it is valid, remember it... + $ipv4list{$usev4}{$arg_ipv4}{$arg_fwv4}{$arg_ifv4}{$arg_webv4}{$arg_cmdv4} = $ipv4; + } else { + warning("%s: unable to determine IPv4 address with strategy usev4=%s", $h, $usev4) + if !$daemon || opt('verbose'); + } + } + # And remember it as the IPv4 address we want to send to the DNS service. + $config{$h}{'wantipv4'} = $ipv4; + } + + if ($usev6 ne 'disabled') { + if (exists $ipv6list{$usev6}{$arg_ipv6}{$arg_fwv6}{$arg_ifv6}{$arg_webv6}{$arg_cmdv6}) { + # If we have already done a get_ipv6() for this, don't do it again. + $ipv6 = $ipv6list{$usev6}{$arg_ipv6}{$arg_fwv6}{$arg_ifv6}{$arg_webv6}{$arg_cmdv6}; + } else { + # Else need to find the IPv6 address... + $ipv6 = get_ipv6($usev6, $h); + if (is_ipv6($ipv6)) { + # And if it is valid, remember it... + $ipv6list{$usev6}{$arg_ipv6}{$arg_fwv6}{$arg_ifv6}{$arg_webv6}{$arg_cmdv6} = $ipv6; + } else { + warning("%s: unable to determine IPv6 address with strategy usev6=%s", $h, $usev6) + if !$daemon || opt('verbose'); + } + } + # And remember it as the IP address we want to send to the DNS service. + $config{$h}{'wantipv6'} = $ipv6; + } + + # DNS service update functions should only have to handle 'wantipv4' and 'wantipv6' + $config{$h}{'wantipv4'} = $ipv4 = $ip if (!$ipv4 && is_ipv4($ip)); + $config{$h}{'wantipv6'} = $ipv6 = $ip if (!$ipv6 && is_ipv6($ip)); + # But we will set 'wantip' to the IPv4 so old functions continue to work until we update them all + $config{$h}{'wantip'} = $ipv4 if (!$ip && $ipv4); + next if !nic_updateable($h, $updateable); push @hosts, $h; - $ips{$ip} = $h; + + $ipsv4{$ipv4} = $h if ($ipv4); + $ipsv6{$ipv6} = $h if ($ipv6); } if (@hosts) { $0 = sprintf("%s - updating %s", $program, join(',', @hosts)); &$update(@hosts); - runpostscript(join ' ', keys %ips); + runpostscript(join ' ', keys %ipsv4, keys %ipsv6); } } foreach my $h (sort keys %config) { @@ -1052,8 +1208,7 @@ sub write_cache { ## merge the updated host entries into the cache. foreach my $h (keys %config) { if (!exists $cache{$h} || $config{$h}{'update'}) { - map { $cache{$h}{$_} = $config{$h}{$_} } @{$config{$h}{'cacheable'}}; - + map { defined($config{$h}{$_}) ? ($cache{$h}{$_} = $config{$h}{$_}) : () } @{$config{$h}{'cacheable'}}; } else { map { $cache{$h}{$_} = $config{$h}{$_} } qw(atime wtime status); } @@ -1341,10 +1496,22 @@ sub init_config { $opt{'quiet'} = 0 if opt('verbose'); ## infer the IP strategy if possible - if (!defined($opt{'use'})) { - $opt{'use'} = 'web' if defined($opt{'web'}); - $opt{'use'} = 'if' if defined($opt{'if'}); - $opt{'use'} = 'ip' if defined($opt{'ip'}); + if (!$opt{'use'}) { + $opt{'use'} = 'web' if ($opt{'web'}); + $opt{'use'} = 'if' if ($opt{'if'}); + $opt{'use'} = 'ip' if ($opt{'ip'}); + } + ## infer the IPv4 strategy if possible + if (!$opt{'usev4'}) { + $opt{'usev4'} = 'webv4' if ($opt{'webv4'}); + $opt{'usev4'} = 'ifv4' if ($opt{'ifv4'}); + $opt{'usev4'} = 'ipv4' if ($opt{'ipv4'}); + } + ## infer the IPv6 strategy if possible + if (!$opt{'usev6'}) { + $opt{'usev6'} = 'webv6' if ($opt{'webv6'}); + $opt{'usev6'} = 'ifv6' if ($opt{'ifv6'}); + $opt{'usev6'} = 'ipv6' if ($opt{'ipv6'}); } ## sanity check @@ -1532,6 +1699,7 @@ sub process_args { sub test_possible_ip { local $opt{'debug'} = 0; + printf "----- Test_possible_ip with 'get_ip' -----\n"; printf "use=ip, ip=%s address is %s\n", opt('ip'), get_ip('ip') // 'NOT FOUND' if defined opt('ip'); @@ -1575,6 +1743,75 @@ sub test_possible_ip { local $opt{'use'} = 'cmd'; printf "use=cmd, cmd=%s address is %s\n", opt('cmd'), get_ip('cmd') // 'NOT FOUND'; } + + # Now force IPv4 + printf "----- Test_possible_ip with 'get_ipv4' ------\n"; + printf "use=ipv4, ipv4=%s address is %s\n", opt('ipv4'), get_ipv4('ipv4') // 'NOT FOUND' + if defined opt('ipv4'); + + { + # Note: The `ip` command adds a `@eth0` suffix to the names of VLAN + # interfaces. That `@eth0` suffix is NOT part of the interface name. + my @ifs = map({ /^[^\s:]*:\s*([^\s:@]+)/ ? $1 : () } + `command -v ip >/dev/null && ip -o link show`); + @ifs = map({ /^([a-zA-Z].*?)(?::?\s.*)?$/ ? $1 : () } + `command -v ifconfig >/dev/null && ifconfig -a`) if $? || !@ifs; + @ifs = () if $?; + warning("failed to get list of interfaces") if !@ifs; + foreach my $if (@ifs) { + local $opt{'ifv4'} = $if; + printf "use=ifv4, ifv4=%s address is %s\n", opt('ifv4'), get_ipv4('ifv4') // 'NOT FOUND'; + } + } + { + local $opt{'usev4'} = 'webv4'; + foreach my $web (sort keys %builtinweb) { + local $opt{'webv4'} = $web; + printf "use=webv4, webv4=$web address is %s\n", get_ipv4('webv4') // 'NOT FOUND' + if ($web !~ "6") ## Don't bother if web site only supports IPv6; + } + printf "use=webv4, webv4=%s address is %s\n", opt('webv4'), get_ipv4('webv4') // 'NOT FOUND' + if ! exists $builtinweb{opt('webv4')}; + } + if (opt('cmdv4')) { + local $opt{'usev4'} = 'cmdv4'; + printf "use=cmdv4, cmdv4=%s address is %s\n", opt('cmdv4'), get_ipv4('cmdv4') // 'NOT FOUND'; + } + + # Now force IPv6 + printf "----- Test_possible_ip with 'get_ipv6' -----\n"; + printf "use=ipv6, ipv6=%s address is %s\n", opt('ipv6'), get_ipv6('ipv6') // 'NOT FOUND' + if defined opt('ipv6'); + + { + # Note: The `ip` command adds a `@eth0` suffix to the names of VLAN + # interfaces. That `@eth0` suffix is NOT part of the interface name. + my @ifs = map({ /^[^\s:]*:\s*([^\s:@]+)/ ? $1 : () } + `command -v ip >/dev/null && ip -o link show`); + @ifs = map({ /^([a-zA-Z].*?)(?::?\s.*)?$/ ? $1 : () } + `command -v ifconfig >/dev/null && ifconfig -a`) if $? || !@ifs; + @ifs = () if $?; + warning("failed to get list of interfaces") if !@ifs; + foreach my $if (@ifs) { + local $opt{'ifv6'} = $if; + printf "use=ifv6, ifv6=%s address is %s\n", opt('ifv6'), get_ipv6('ifv6') // 'NOT FOUND'; + } + } + { + local $opt{'usev6'} = 'webv6'; + foreach my $web (sort keys %builtinweb) { + local $opt{'webv6'} = $web; + printf "use=webv6, webv6=$web address is %s\n", get_ipv6('webv6') // 'NOT FOUND' + if ($web !~ "4"); ## Don't bother if web site only supports IPv4 + } + printf "use=webv6, webv6=%s address is %s\n", opt('webv6'), get_ipv6('webv6') // 'NOT FOUND' + if ! exists $builtinweb{opt('webv6')}; + } + if (opt('cmdv6')) { + local $opt{'usev6'} = 'cmdv6'; + printf "use=cmdv6, cmdv6=%s address is %s\n", opt('cmdv6'), get_ipv6('cmdv6') // 'NOT FOUND'; + } + exit 0 unless opt('debug'); } ###################################################################### @@ -1942,6 +2179,14 @@ sub check_value { $value = lc $value; return undef if !exists $ip_strategies{$value}; + } elsif ($type eq T_USEV4) { + $value = lc $value; + return undef if ! exists $ipv4_strategies{$value}; + + } elsif ($type eq T_USEV6) { + $value = lc $value; + return undef if ! exists $ipv6_strategies{$value}; + } elsif ($type eq T_FILE) { return undef if $value eq ""; @@ -1956,6 +2201,13 @@ sub check_value { } elsif ($type eq T_IP) { return undef if !is_ipv4($value) && !is_ipv6($value); + + } elsif ($type eq T_IPV4) { + return undef if !is_ipv4($value); + + } elsif ($type eq T_IPV6) { + return undef if !is_ipv6($value); + } return $value; } @@ -2448,6 +2700,7 @@ sub fetch_via_curl { ###################################################################### sub get_ip { my $use = lc shift; + $use = 'disabled' if ($use eq 'no'); # backward compatibility my $h = shift; my ($ip, $arg, $reply, $url, $skip) = (undef, opt($use, $h), ''); $arg = '' unless $arg; @@ -2455,7 +2708,7 @@ sub get_ip { if ($use eq 'ip') { $ip = opt('ip', $h); if (!is_ipv4($ip) && !is_ipv6($ip)) { - warning("'%s' is not a valid IPv4 or IPv6 address", $ip); + warning("'%s' is not a valid IPv4 or IPv6 address", $ip // ''); $ip = undef; } $arg = 'ip'; @@ -2534,6 +2787,10 @@ sub get_ip { ) // ''; $arg = $url; + } elsif ($use eq 'disabled') { + ## This is a no-op... Do not get an IP address for this host/service + $reply = ''; + } else { $url = opt('fw', $h) // ''; $skip = opt('fw-skip', $h) // ''; @@ -2571,7 +2828,6 @@ sub get_ip { return $ip; } - ###################################################################### ## Regex to find IPv4 address. Accepts embedded leading zeros. ###################################################################### @@ -2873,10 +3129,242 @@ sub get_ip_from_interface { return extract_ipv6($sorted[0]); } +###################################################################### +## get_ipv4 +###################################################################### +sub get_ipv4 { + my $usev4 = lc(shift); ## Method to obtain IP address + my $h = shift; ## Host/service making the request + + my $ipv4 = undef; ## Found IPv4 address + my $reply = ''; ## Text returned from various methods + my $url = ''; ## URL of website or firewall + my $skip = ''; ## Regex of pattern to skip before looking for IP + my $arg = opt($usev4, $h) // ''; ## Value assigned to the "usev4" method + + if ($usev4 eq 'ipv4') { + ## Static IPv4 address is provided in "ipv4=
" + $ipv4 = $arg; + if (!is_ipv4($ipv4)) { + warning("'%s' is not a valid IPv4",$ipv4 // ''); + $ipv4 = undef; + } + $arg = 'ipv4'; # For debug message at end of function + + } elsif ($usev4 eq 'ifv4') { + ## Obtain IPv4 address from interface mamed in "ifv4=" + warning("'if-skip' is deprecated and does nothing for IPv4") if (opt('verbose') && opt('if-skip', $h)); + $ipv4 = get_ip_from_interface($arg,4); + + } elsif ($usev4 eq 'cmdv4') { + ## Obtain IPv4 address by executing the command in "cmdv4=" + warning("'cmd-skip' is deprecated and does nothing for IPv4") if (opt('verbose') && opt('cmd-skip', $h)); + if ($arg) { + my $sys_cmd = quotemeta($arg); + $reply = qx{$sys_cmd}; + $reply = '' if $?; + } + + } elsif ($usev4 eq 'webv4') { + ## Obtain IPv4 address by accessing website at url in "webv4=" + $url = $arg; + $skip = opt('webv4-skip', $h) // ''; + if (exists $builtinweb{$url}) { + $skip = $builtinweb{$url}->{'skip'} unless $skip; + $url = $builtinweb{$url}->{'url'}; + $arg = $url; + } + if ($url) { + $reply = geturl( proxy => opt('proxy', $h), + url => $url, + ipversion => 4, # when using a URL to find IPv4 address we should force use of IPv4 + ssl_validate => opt('ssl-validate', $h), + ) // ''; + } + + } elsif ($usev4 eq 'cisco' || $usev4 eq 'cisco-asa') { + # Stuff added to support Cisco router ip http or ASA https daemon + # User fw-login should only have level 1 access to prevent + # password theft. This is pretty harmless. + warning("'if' does nothing for IPv4. Use 'ifv4'") if (opt('if', $h)); + warning("'fw' does nothing for IPv4. Use 'fwv4'") if (opt('fw', $h)); + warning("'fw-skip' does nothing for IPv4. Use 'fwv4-skip'") if (opt('fw-skip', $h)); + my $queryif = opt('ifv4', $h) // opt('if', $h); + $skip = opt('fwv4-skip', $h) // opt('fw-skip', $h) // ''; + # Convert slashes to protected value "\/" + $queryif =~ s%\/%\\\/%g; + # Protect special HTML characters (like '?') + $queryif =~ s/([\?&= ])/sprintf("%%%02x", ord($1))/ge; + if ($usev4 eq 'cisco') { + $url = "http://" . (opt('fwv4', $h) // opt('fw', $h)) . "/level/1/exec/show/ip/interface/brief/${queryif}/CR"; + } else { + $url = "https://" . (opt('fwv4', $h) // opt('fw', $h)) . "/exec/show%20interface%20${queryif}"; + } + $arg = $url; + $reply = geturl( + url => $url, + login => opt('fw-login', $h), + password => opt('fw-password', $h), + ipversion => 4, # when using a URL to find IPv4 address we should force use of IPv4 + ignore_ssl_option => 1, + ssl_validate => opt('ssl-validate', $h), + ) // ''; + + } elsif ($usev4 eq 'disabled') { + ## This is a no-op... Do not get an IPv4 address for this host/service + $reply = ''; + + } else { + warning("'fw' does nothing for IPv4. Use 'fwv4'") if (opt('fw', $h)); + warning("'fw-skip' does nothing for IPv4. Use 'fwv4-skip'") if (opt('fw-skip', $h)); + $url = opt('fwv4', $h) // opt('fw', $h) // ''; + $skip = opt('fwv4-skip', $h) // opt('fw-skip', $h) // ''; + + if (exists $builtinfw{$usev4}) { + $skip = $builtinfw{$usev4}->{'skip'} unless $skip; + $url = "http://${url}" . $builtinfw{$usev4}->{'url'} unless $url =~ /\//; + } + $arg = $url; + if ($url) { + $reply = geturl( + url => $url, + login => opt('fw-login', $h), + password => opt('fw-password', $h), + ipversion => 4, # when using a URL to find IPv4 address we should force use of IPv4 + ignore_ssl_option => 1, + ssl_validate => opt('ssl-validate', $h), + ) // ''; + } + } + + ## Set to loopback address if no text set yet + $reply = '0.0.0.0' if !defined($reply); + if (($skip // '') ne '') { + $skip =~ s/ /\\s/is; + $reply =~ s/^.*?${skip}//is; + } + ## If $ipv4 not set yet look for IPv4 address in the $reply text + $ipv4 //= extract_ipv4($reply); + ## Return undef for loopback address unless statically assigned by "ipv4=0.0.0.0" + $ipv4 = undef if (($usev4 ne 'ipv4') && (($ipv4 // '') eq '0.0.0.0')); + debug("get_ipv4: using (%s, %s) reports %s", $usev4, $arg, $ipv4 // ""); + return $ipv4; +} + +###################################################################### +## get_ipv6 +###################################################################### +sub get_ipv6 { + my $usev6 = lc(shift); ## Method to obtain IP address + $usev6 = 'disabled' if ($usev6 eq 'no'); # backward compatibility + my $h = shift; ## Host/service making the request + + my $ipv6 = undef; ## Found IPv6 address + my $reply = ''; ## Text returned from various methods + my $url = ''; ## URL of website or firewall + my $skip = ''; ## Regex of pattern to skip before looking for IP + my $arg = opt($usev6, $h) // ''; ## Value assigned to the "usev6" method + + if ($usev6 eq 'ipv6' || $usev6 eq 'ip') { + ## Static IPv6 address is provided in "ipv6=
" + if ($usev6 eq 'ip') { + warning("'usev6=ip' is deprecated. Use 'usev6=ipv6'"); + $usev6 = 'ipv6'; + ## If there is a value for ipv6= use that, else use value for ip= + $arg = opt($usev6, $h) // $arg; + } + $ipv6 = $arg; + if (!is_ipv6($ipv6)) { + warning("'%s' is not a valid IPv6",$ipv6 // ''); + $ipv6 = undef; + } + $arg = 'ipv6'; # For debug message at end of function + + } elsif ($usev6 eq 'ifv6' || $usev6 eq 'if' ) { + ## Obtain IPv6 address from interface mamed in "ifv6=" + if ($usev6 eq 'if') { + warning("'usev6=if' is deprecated. Use 'usev6=ifv6'"); + $usev6 = 'ifv6'; + ## If there is a value for ifv6= use that, else use value for if= + $arg = opt($usev6, $h) // $arg; + } + warning("'if-skip' is deprecated and does nothing for IPv6") if (opt('verbose') && opt('if-skip', $h)); + $ipv6 = get_ip_from_interface($arg,6); + + } elsif ($usev6 eq 'cmdv6' || $usev6 eq 'cmd') { + ## Obtain IPv6 address by executing the command in "cmdv6=" + if ($usev6 eq 'cmd') { + warning("'usev6=cmd' is deprecated. Use 'usev6=cmdv6'"); + $usev6 = 'cmdv6'; + ## If there is a value for cmdv6= use that, else use value for cmd= + $arg = opt($usev6, $h) // $arg; + } + warning("'cmd-skip' is deprecated and does nothing for IPv6") if (opt('verbose') && opt('cmd-skip', $h)); + if ($arg) { + my $sys_cmd = quotemeta($arg); + $reply = qx{$sys_cmd}; + $reply = '' if $?; + } + + } elsif ($usev6 eq 'webv6' || $usev6 eq 'web') { + ## Obtain IPv6 address by accessing website at url in "webv6=" + if ($usev6 eq 'web') { + warning("'usev6=web' is deprecated. Use 'usev6=webv6'"); + $usev6 = 'webv6'; + ## If there is a value for webv6= use that, else use value for web= + $arg = opt($usev6, $h) // $arg; + } + warning("'web-skip' does nothing for IPv6. Use 'webv6-skip'") if (opt('web-skip', $h)); + $url = $arg; + $skip = opt('webv6-skip', $h) // ''; + if (exists $builtinweb{$url}) { + $skip = $builtinweb{$url}->{'skip'} unless $skip; + $url = $builtinweb{$url}->{'url'}; + $arg = $url; + } + if ($url) { + $reply = geturl( + proxy => opt('proxy'), + url => $url, + ipversion => 6, # when using a URL to find IPv6 address we should force use of IPv6 + ssl_validate => opt('ssl-validate', $h), + ) // ''; + } + + } elsif ($usev6 eq 'cisco' || $usev6 eq 'cisco-asa') { + warning("'usev6=cisco' and 'usev6=cisco-asa' are not implemented and do nothing"); + $reply = ''; + + } elsif ($usev6 eq 'disabled') { + ## This is a no-op... Do not get an IPv6 address for this host/service + warning("'usev6=no' is deprecated. Use 'usev6=disabled'") if ($usev6 eq 'no'); + $reply = ''; + + } else { + warning("'usev6=%s' is not implemented and does nothing", $usev6); + $reply = ''; + + } + + ## Set to loopback address if no text set yet + $reply = '::' if !defined($reply); + if (($skip // '') ne '') { + $skip =~ s/ /\\s/is; + $reply =~ s/^.*?${skip}//is; + } + ## If $ipv6 not set yet look for IPv6 address in the $reply text + $ipv6 //= extract_ipv6($reply); + ## Return undef for loopback address unless statically assigned by "ipv6=::" + $ipv6 = undef if (($usev6 ne 'ipv6') && (($ipv6 // '') eq '::')); + debug("get_ipv6: using (%s, %s) reports %s", $usev6, $arg, $ipv6 // ""); + return $ipv6; +} + ###################################################################### ## group_hosts_by ###################################################################### sub group_hosts_by { +##TODO - Update for wantipv4 and wantipv6 my ($hosts, $attributes) = @_; my %attrs = (map({ ($_ => 1) } @$attributes), 'wantip' => 1); my @attrs = sort(keys(%attrs)); @@ -2984,12 +3472,35 @@ EoEXAMPLE } ###################################################################### ## nic_updateable +## Returns true if we can go ahead and update the IP address at server ###################################################################### sub nic_updateable { my $host = shift; my $sub = shift; my $update = 0; my $ip = $config{$host}{'wantip'}; + my $ipv4 = $config{$host}{'wantipv4'}; + my $ipv6 = $config{$host}{'wantipv6'}; + my $use = opt('use', $host) // 'disabled'; + my $usev4 = opt('usev4', $host) // 'disabled'; + my $usev6 = opt('usev6', $host) // 'disabled'; + $use = 'disabled' if ($use eq 'no'); # backward compatibility + $usev6 = 'disabled' if ($usev6 eq 'no'); # backward compatibility + + # If we have a valid IP address and we have previously warned that it was invalid. + # reset the warning count back to zero. + if (($use ne 'disabled') && $ip && $warned_ip{$host}) { + $warned_ip{$host} = 0; + warning("IP address for %s valid: %s. Reset warning count", $host, $ip); + } + if (($usev4 ne 'disabled') && $ipv4 && $warned_ipv4{$host}) { + $warned_ipv4{$host} = 0; + warning("IPv4 address for %s valid: %s. Reset warning count", $host, $ipv4); + } + if (($usev6 ne 'disabled') && $ipv6 && $warned_ipv6{$host}) { + $warned_ipv6{$host} = 0; + warning("IPv6 address for %s valid: %s. Reset warning count", $host, $ipv6); + } if ($config{$host}{'login'} eq '') { warning("null login name specified for host %s.", $host); @@ -3021,7 +3532,9 @@ sub nic_updateable { ); $update = 1; - } elsif (!exists($cache{$host}{'ip'}) || $cache{$host}{'ip'} ne $ip) { + } elsif ( ($use ne 'disabled') + && ((!exists($cache{$host}{'ip'})) || ("$cache{$host}{'ip'}" ne "$ip"))) { + ## Check whether to update IP address for the "use" method" if (($cache{$host}{'status'} eq 'good') && !interval_expired($host, 'mtime', 'min-interval')) { @@ -3036,17 +3549,117 @@ sub nic_updateable { $cache{$host}{'warned-min-interval'} = $now; - } elsif (($cache{$host}{'status'} ne 'good') && !interval_expired($host, 'atime', 'min-error-interval')) { + } elsif (($cache{$host}{'status'} ne 'good') && + !interval_expired($host, 'atime', 'min-error-interval')) { - warning("skipping update of %s from %s to %s.\nlast updated %s but last attempt on %s failed.\nWait at least %s between update attempts.", + if ( opt('verbose') + || ( ! $cache{$host}{'warned-min-error-interval'} + && (($warned_ip{$host} // 0) < $inv_ip_warn_count)) ) { + + warning("skipping update of %s from %s to %s.\nlast updated %s but last attempt on %s failed.\nWait at least %s between update attempts.", + $host, + ($cache{$host}{'ip'} ? $cache{$host}{'ip'} : ''), + $ip, + ($cache{$host}{'mtime'} ? prettytime($cache{$host}{'mtime'}) : ''), + ($cache{$host}{'atime'} ? prettytime($cache{$host}{'atime'}) : ''), + prettyinterval($config{$host}{'min-error-interval'}) + ); + if (!$ip && !opt('verbose')) { + $warned_ip{$host} = ($warned_ip{$host} // 0) + 1; + warning("IP address for %s undefined. Warned %s times, suppressing further warnings", $host, $inv_ip_warn_count) + if ($warned_ip{$host} >= $inv_ip_warn_count); + } + } + + $cache{$host}{'warned-min-error-interval'} = $now; + + } else { + $update = 1; + } + + } elsif ( ($usev4 ne 'disabled') + && ((!exists($cache{$host}{'ipv4'})) || ("$cache{$host}{'ipv4'}" ne "$ipv4"))) { + ## Check whether to update IPv4 address for the "usev4" method" + if (($cache{$host}{'status-ipv4'} eq 'good') && + !interval_expired($host, 'mtime', 'min-interval')) { + + warning("skipping update of %s from %s to %s.\nlast updated %s.\nWait at least %s between update attempts.", $host, - ($cache{$host}{'ip'} ? $cache{$host}{'ip'} : ''), - $ip, + ($cache{$host}{'ipv4'} ? $cache{$host}{'ipv4'} : ''), + $ipv4, ($cache{$host}{'mtime'} ? prettytime($cache{$host}{'mtime'}) : ''), - ($cache{$host}{'atime'} ? prettytime($cache{$host}{'atime'}) : ''), - prettyinterval($config{$host}{'min-error-interval'}) + prettyinterval($config{$host}{'min-interval'}) ) - if opt('verbose') || !($cache{$host}{'warned-min-error-interval'} // 0); + if opt('verbose') || !($cache{$host}{'warned-min-interval'} // 0); + + $cache{$host}{'warned-min-interval'} = $now; + + } elsif (($cache{$host}{'status-ipv4'} ne 'good') && + !interval_expired($host, 'atime', 'min-error-interval')) { + + if ( opt('verbose') + || ( ! $cache{$host}{'warned-min-error-interval'} + && (($warned_ipv4{$host} // 0) < $inv_ip_warn_count)) ) { + + warning("skipping update of %s from %s to %s.\nlast updated %s but last attempt on %s failed.\nWait at least %s between update attempts.", + $host, + ($cache{$host}{'ipv4'} ? $cache{$host}{'ipv4'} : ''), + $ipv4, + ($cache{$host}{'mtime'} ? prettytime($cache{$host}{'mtime'}) : ''), + ($cache{$host}{'atime'} ? prettytime($cache{$host}{'atime'}) : ''), + prettyinterval($config{$host}{'min-error-interval'}) + ); + if (!$ipv4 && !opt('verbose')) { + $warned_ipv4{$host} = ($warned_ipv4{$host} // 0) + 1; + warning("IPv4 address for %s undefined. Warned %s times, suppressing further warnings", $host, $inv_ip_warn_count) + if ($warned_ipv4{$host} >= $inv_ip_warn_count); + } + } + + $cache{$host}{'warned-min-error-interval'} = $now; + + } else { + $update = 1; + } + + } elsif ( ($usev6 ne 'disabled') + && ((!exists($cache{$host}{'ipv6'})) || ("$cache{$host}{'ipv6'}" ne "$ipv6"))) { + ## Check whether to update IPv6 address for the "usev6" method" + if (($cache{$host}{'status-ipv6'} eq 'good') && + !interval_expired($host, 'mtime', 'min-interval')) { + + warning("skipping update of %s from %s to %s.\nlast updated %s.\nWait at least %s between update attempts.", + $host, + ($cache{$host}{'ipv6'} ? $cache{$host}{'ipv6'} : ''), + $ipv6, + ($cache{$host}{'mtime'} ? prettytime($cache{$host}{'mtime'}) : ''), + prettyinterval($config{$host}{'min-interval'}) + ) + if opt('verbose') || !($cache{$host}{'warned-min-interval'} // 0); + + $cache{$host}{'warned-min-interval'} = $now; + + } elsif (($cache{$host}{'status-ipv6'} ne 'good') && + !interval_expired($host, 'atime', 'min-error-interval')) { + + if ( opt('verbose') + || ( ! $cache{$host}{'warned-min-error-interval'} + && (($warned_ipv6{$host} // 0) < $inv_ip_warn_count)) ) { + + warning("skipping update of %s from %s to %s.\nlast updated %s but last attempt on %s failed.\nWait at least %s between update attempts.", + $host, + ($cache{$host}{'ipv6'} ? $cache{$host}{'ipv6'} : ''), + $ipv6, + ($cache{$host}{'mtime'} ? prettytime($cache{$host}{'mtime'}) : ''), + ($cache{$host}{'atime'} ? prettytime($cache{$host}{'atime'}) : ''), + prettyinterval($config{$host}{'min-error-interval'}) + ); + if (!$ipv6 && !opt('verbose')) { + $warned_ipv6{$host} = ($warned_ipv6{$host} // 0) + 1; + warning("IPv6 address for %s undefined. Warned %s times, suppressing further warnings", $host, $inv_ip_warn_count) + if ($warned_ipv6{$host} >= $inv_ip_warn_count); + } + } $cache{$host}{'warned-min-error-interval'} = $now; @@ -3068,13 +3681,27 @@ sub nic_updateable { $update = 1; } else { - success("%s: skipped: IP address was already set to %s.", $host, $ip) - if opt('verbose'); + if (opt('verbose')) { + if ($use ne 'disabled') { + success("%s: skipped: IP address was already set to %s.", $host, $ip); + } + if ($usev4 ne 'disabled') { + success("%s: skipped: IPv4 address was already set to %s.", $host, $ipv6); + } + if ($usev6 ne 'disabled') { + success("%s: skipped: IPv6 address was already set to %s.", $host, $ipv6); + } + } } + $config{$host}{'status'} = $cache{$host}{'status'} // ''; + $config{$host}{'status-ipv4'} = $cache{$host}{'status-ipv4'} // ''; + $config{$host}{'status-ipv6'} = $cache{$host}{'status-ipv6'} // ''; $config{$host}{'update'} = $update; if ($update) { $config{$host}{'status'} = 'noconnect'; + $config{$host}{'status-ipv4'} = 'noconnect'; + $config{$host}{'status-ipv6'} = 'noconnect'; $config{$host}{'atime'} = $now; $config{$host}{'wtime'} = 0; $config{$host}{'warned-min-interval'} = 0; @@ -4333,12 +4960,14 @@ EoEXAMPLE ## hostname2.example.com|5.6.7.8|http://example/update/url3 ## hostname2.example.com|9.10.11.12|http://example/update/url4 ## hostname3.example.com|cafe::f00d|http://example/update/url5 +## hostname4.example.com|NULL|http://example/update/url6 ## ## The record's columns are separated by '|'. The first is the hostname, the second is the current ## address, and the third is the record-specific update URL. There can be multiple records for the -## same host, and they can even have the same address type. Any record can be updated to hold -## either type of address (e.g., if given an IPv6 address the record will automatically become an -## AAAA record). +## same host, and they can even have the same address type. To update an IP address the record +## must already exist of the type we want to update... We will not change a record type from +## an IPv4 to IPv6 or viz versa. Records may exist with a NULL address which we will allow to be +## updated with an IPv4 address, not an IPv6. ## ## The second step is to visit the appropriate record's update URL with ## ?address= appended. "Updated" in the result means success, "fail" means @@ -4354,7 +4983,10 @@ sub nic_freedns_update { my $url_tmpl = "http://$config{$_[0]}{'server'}/api/?action=getdyndns&v=2&sha="; my $creds = sha1_hex("$config{$_[0]}{'login'}|$config{$_[0]}{'password'}"); (my $url = $url_tmpl) =~ s//$creds/; - my $reply = geturl(proxy => opt('proxy'), url => $url); + + my $reply = geturl(proxy => opt('proxy'), + url => $url + ); my $record_list_error = ''; if ($reply && header_ok($_[0], $reply)) { $reply =~ s/^.*?\n\n//s; # Strip the headers. @@ -4363,7 +4995,8 @@ sub nic_freedns_update { next if ($#rec < 2); my $recs = is_ipv6($rec[1]) ? \%recs_ipv6 : \%recs_ipv4; $recs->{$rec[0]} = \@rec; - debug("host: %s, current address: %s, update URL: %s", @rec); + # Update URL contains credentials that don't require login to use, so best to hide. + debug("host: %s, current address: %s, update URL: ", $rec[0], $rec[1]); } if (keys(%recs_ipv4) + keys(%recs_ipv6) == 0) { chomp($reply); @@ -4374,54 +5007,61 @@ sub nic_freedns_update { } foreach my $h (@_) { - if (!$h) { next } - my $ip = delete $config{$h}{'wantip'}; - - info("%s: setting IP address to %s", $h, $ip); + next if (!$h); + my $ipv4 = delete $config{$h}{'wantipv4'}; + my $ipv6 = delete $config{$h}{'wantipv6'}; if ($record_list_error ne '') { - $config{$h}{'status'} = 'failed'; + $config{$h}{'status-ipv4'} = 'failed' if ($ipv4); + $config{$h}{'status-ipv6'} = 'failed' if ($ipv6); failed("updating %s: %s", $h, $record_list_error); next; } - # If there is a record with matching type then update it, otherwise let - # freedns convert the record to the desired type. - my $rec = is_ipv6($ip) - ? ($recs_ipv6{$h} // $recs_ipv4{$h}) - : ($recs_ipv4{$h} // $recs_ipv6{$h}); - if (!defined($rec)) { - $config{$h}{'status'} = 'failed'; - failed("updating %s: host record does not exist", $h); - next; - } - if ($ip eq $rec->[1]) { - $config{$h}{'ip'} = $ip; - $config{$h}{'mtime'} = $now; - $config{$h}{'status'} = 'good'; - success("update not necessary %s: good: IP address already set to %s", $h, $ip) - if (!$daemon || opt('verbose')); - } else { - my $url = $rec->[2] . "&address=" . $ip; - debug("Update: %s", $url); - my $reply = geturl(proxy => opt('proxy'), url => $url); - if (!defined($reply) || !$reply || !header_ok($h, $reply)) { - $config{$h}{'status'} = 'failed'; - failed("updating %s: Could not connect to %s.", $h, $url); + # IPv4 and IPv6 handling are similar enough to do in a loop... + foreach my $ip ($ipv4, $ipv6) { + next if (!$ip); + my $ipv = ($ip eq ($ipv6 // '')) ? '6' : '4'; + my $type = ($ip eq ($ipv6 // '')) ? 'AAAA' : 'A'; + my $rec = ($ip eq ($ipv6 // '')) ? $recs_ipv6{$h} + : $recs_ipv4{$h}; + if (!$rec) { + failed("updating %s: Cannot set IPv$ipv to %s No '$type' record at FreeDNS", $h, $ip); next; } - $reply =~ s/^.*?\n\n//s; # Strip the headers. - if ($reply =~ /Updated.*$h.*to.*$ip/) { - $config{$h}{'ip'} = $ip; - $config{$h}{'mtime'} = $now; - $config{$h}{'status'} = 'good'; - success("updating %s: good: IP address set to %s", $h, $ip); + info("updating %s: setting IP address to %s", $h, $ip); + $config{$h}{"status-ipv$ipv"} = 'failed'; + + if ($ip eq $rec->[1]) { + $config{$h}{"ipv$ipv"} = $ip; + $config{$h}{'mtime'} = $now; + $config{$h}{"status-ipv$ipv"} = 'good'; + success("updating %s: update not necessary, '$type' record already set to %s", $h, $ip) + if (!$daemon || opt('verbose')); } else { - $config{$h}{'status'} = 'failed'; - warning("SENT: %s", $url) unless opt('verbose'); - warning("REPLIED: %s", $reply); - failed("updating %s: Invalid reply.", $h); + my $url = $rec->[2] . "&address=" . $ip; + ($url_tmpl = $url) =~ s/\?.*\&/?&/; # redact unique update token + debug("updating: %s", $url_tmpl); + + my $reply = geturl(proxy => opt('proxy'), + url => $url + ); + if ($reply && header_ok($h, $reply)) { + $reply =~ s/^.*?\n\n//s; # Strip the headers. + if ($reply =~ /Updated.*$h.*to.*$ip/) { + $config{$h}{"ipv$ipv"} = $ip; + $config{$h}{'mtime'} = $now; + $config{$h}{"status-ipv$ipv"} = 'good'; + success("updating %s: good: IPv$ipv address set to %s", $h, $ip); + } else { + warning("SENT: %s", $url_tmpl) unless opt('verbose'); + warning("REPLIED: %s", $reply); + failed("updating %s: Invalid reply.", $h); + } + } else { + failed("updating %s: Could not connect to %s.", $h, $url_tmpl); + } } } } @@ -4759,7 +5399,6 @@ sub nic_cloudflare_update { my @hosts = @{$groups{$sig}}; my $hosts = join(',', @hosts); my $key = $hosts[0]; - my $ip = $config{$key}{'wantip'}; my $headers = "Content-Type: application/json\n"; if ($config{$key}{'login'} eq 'token') { @@ -4772,102 +5411,104 @@ sub nic_cloudflare_update { # FQDNs for my $domain (@hosts) { (my $hostname = $domain) =~ s/\.$config{$key}{zone}$//; - delete $config{$domain}{'wantip'}; + my $ipv4 = delete $config{$domain}{'wantipv4'}; + my $ipv6 = delete $config{$domain}{'wantipv6'}; - info("setting IP address to %s for %s", $ip, $domain); - verbose("UPDATE:", "updating %s", $domain); + info("getting Cloudflare Zone ID for %s", $domain); # Get zone ID my $url = "https://$config{$key}{'server'}/zones?"; - $url .= "name=".$config{$key}{'zone'}; + $url .= "name=" . $config{$key}{'zone'}; - my $reply = geturl(proxy => opt('proxy'), url => $url, headers => $headers); - unless ($reply) { + my $reply = geturl(proxy => opt('proxy'), + url => $url, + headers => $headers + ); + unless ($reply && header_ok($domain, $reply)) { failed("updating %s: Could not connect to %s.", $domain, $config{$key}{'server'}); next; } - next if !header_ok($domain, $reply); # Strip header $reply =~ s/^.*?\n\n//s; - my $response = eval { decode_json($reply) }; - if (!defined $response || !defined $response->{result}) { - failed("invalid json or result."); + my $response = eval {decode_json($reply)}; + unless ($response && $response->{result}) { + failed("updating %s: invalid json or result.", $domain); next; } # Pull the ID out of the json, messy - my ($zone_id) = map { $_->{name} eq $config{$key}{'zone'} ? $_->{id} : () } @{$response->{result}}; + my ($zone_id) = map {$_->{name} eq $config{$key}{'zone'} ? $_->{id} : ()} @{$response->{result}}; unless ($zone_id) { failed("updating %s: No zone ID found.", $config{$key}{'zone'}); next; } - info("zone ID is %s", $zone_id); + info("Zone ID is %s", $zone_id); - # Get DNS record ID - $url = "https://$config{$key}{'server'}/zones/$zone_id/dns_records?"; - if (is_ipv6($ip)) { - $url .= "type=AAAA&name=$domain"; - } else { - $url .= "type=A&name=$domain"; + + # IPv4 and IPv6 handling are similar enough to do in a loop... + foreach my $ip ($ipv4, $ipv6) { + next if (!$ip); + my $ipv = ($ip eq ($ipv6 // '')) ? '6' : '4'; + my $type = ($ip eq ($ipv6 // '')) ? 'AAAA' : 'A'; + + info("updating %s: setting IPv$ipv address to %s", $domain, $ip); + $config{$domain}{"status-ipv$ipv"} = 'failed'; + + # Get DNS 'A' or 'AAAA' record ID + $url = "https://$config{$key}{'server'}/zones/$zone_id/dns_records?"; + $url .= "type=$type&name=$domain"; + $reply = geturl(proxy => opt('proxy'), + url => $url, + headers => $headers + ); + unless ($reply && header_ok($domain, $reply)) { + failed("updating %s: Could not connect to %s.", $domain, $config{$key}{'server'}); + next; + } + # Strip header + $reply =~ s/^.*?\n\n//s; + $response = eval {decode_json($reply)}; + unless ($response && $response->{result}) { + failed("updating %s: invalid json or result.", $domain); + next; + } + # Pull the ID out of the json, messy + my ($dns_rec_id) = map {$_->{name} eq $domain ? $_->{id} : ()} @{$response->{result}}; + unless($dns_rec_id) { + failed("updating %s: Cannot set IPv$ipv to %s No '$type' record at Cloudflare", $domain, $ip); + next; + } + debug("updating %s: DNS '$type' record ID: $dns_rec_id", $domain); + # Set domain + $url = "https://$config{$key}{'server'}/zones/$zone_id/dns_records/$dns_rec_id"; + my $data = "{\"content\":\"$ip\"}"; + $reply = geturl(proxy => opt('proxy'), + url => $url, + headers => $headers, + method => "PATCH", + data => $data + ); + unless ($reply && header_ok($domain, $reply)) { + failed("updating %s: Could not connect to %s.", $domain, $config{$domain}{'server'}); + next; + } + # Strip header + $reply =~ s/^.*?\n\n//s; + $response = eval {decode_json($reply)}; + if ($response && $response->{result}) { + success("updating %s: IPv$ipv address set to %s", $domain, $ip); + $config{$domain}{"ipv$ipv"} = $ip; + $config{$domain}{'mtime'} = $now; + $config{$domain}{"status-ipv$ipv"} = 'good'; + } else { + failed("updating %s: invalid json or result.", $domain); + } } - - $reply = geturl(proxy => opt('proxy'), url => $url, headers => $headers); - unless ($reply) { - failed("updating %s: Could not connect to %s.", $domain, $config{$key}{'server'}); - next; - } - next if !header_ok($domain, $reply); - - # Strip header - $reply =~ s/^.*?\n\n//s; - $response = eval { decode_json($reply) }; - if (!defined $response || !defined $response->{result}) { - failed("invalid json or result."); - next; - } - - # Pull the ID out of the json, messy - my ($dns_rec_id) = map { $_->{name} eq $domain ? $_->{id} : () } @{$response->{result}}; - unless ($dns_rec_id) { - failed("updating %s: No DNS record ID found.", $domain); - next; - } - info("DNS record ID is %s", $dns_rec_id); - - # Set domain - $url = "https://$config{$key}{'server'}/zones/$zone_id/dns_records/$dns_rec_id"; - my $data = "{\"content\":\"$ip\"}"; - $reply = geturl( - proxy => opt('proxy'), - url => $url, - headers => $headers, - method => "PATCH", - data => $data, - ); - unless ($reply) { - failed("updating %s: Could not connect to %s.", $domain, $config{$domain}{'server'}); - next; - } - next if !header_ok($domain, $reply); - - # Strip header - $reply =~ s/^.*?\n\n//s; - $response = eval { decode_json($reply) }; - if (!defined $response || !defined $response->{result}) { - failed("invalid json or result."); - } else { - success("%s -- Updated Successfully to %s", $domain, $ip); - - } - - # Cache - $config{$domain}{'ip'} = $ip; - $config{$domain}{'mtime'} = $now; - $config{$domain}{'status'} = 'good'; } } } + ###################################################################### ## nic_yandex_examples ######################################################################