diff --git a/ChangeLog.md b/ChangeLog.md index 19633f3..cc15c1f 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -34,6 +34,15 @@ repository history](https://github.com/ddclient/ddclient/commits/master). * Deprecated built-in web IP discovery services are not listed in the output of `--list-web-services`. [#682](https://github.com/ddclient/ddclient/pull/682) + * `dyndns2`: Support for "wait" response lines has been removed. The Dyn + documentation does not mention such responses, and the code to handle them, + untouched since at least 2006, is believed to be obsolete. + [#709](https://github.com/ddclient/ddclient/pull/709) + * `dyndns2`: The obsolete `static` and `custom` options have been removed. + Setting the options may produce a warning. + [#709](https://github.com/ddclient/ddclient/pull/709) + * The diagnostic `--geturl` command-line argument was removed. + [#TODO](https://github.com/ddclient/ddclient/pull/TODO) ### New features @@ -70,6 +79,8 @@ repository history](https://github.com/ddclient/ddclient/commits/master). [#703](https://github.com/ddclient/ddclient/pull/703) * `ddns.fm`: New `protocol` option for updating [DDNS.FM](https://ddns.fm/) records. [#695](https://github.com/ddclient/ddclient/pull/695) + * `inwx`: New `protocol` option for updating [INWX](https://www.inwx.com/) + records. [#690](https://github.com/ddclient/ddclient/pull/690) ### Bug fixes diff --git a/README.md b/README.md index 5b3275a..07357b9 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Dynamic DNS services currently supported include: * [Google](https://domains.google) * [Hurricane Electric](https://dns.he.net) * [Infomaniak](https://faq.infomaniak.com/2376) +* [INWX](https://www.inwx.com/) * [Loopia](https://www.loopia.se) * [Mythic Beasts](https://www.mythic-beasts.com/support/api/dnsv2/dynamic-dns) * [NameCheap](https://www.namecheap.com) @@ -167,41 +168,56 @@ This issue arises when using the `use` parameter in the config and using one of ## TROUBLESHOOTING - 1. enable debugging and verbose messages: ``$ ddclient --daemon=0 --debug --verbose --noquiet`` + * Enable debugging and verbose messages: `ddclient --daemon=0 --debug --verbose` - 2. Do you need to specify a proxy? - If so, just add a ``proxy=your.isp.proxy`` to the ddclient.conf file. + * Do you need to specify a proxy? + If so, just add a `proxy=your.isp.proxy` to the `ddclient.conf` file. - 3. Define the IP address of your router with ``fw=xxx.xxx.xxx.xxx`` in - ``/etc/ddclient/ddclient.conf`` and then try ``$ ddclient --daemon=0 --query`` to see if the router status web page can be understood. + * Define the IP address of your router with `fwv4=xxx.xxx.xxx.xxx` in + `/etc/ddclient/ddclient.conf` and then try `$ ddclient --daemon=0 --query` + to see if the router status web page can be understood. - 4. Need support for another router/firewall? - Define the router status page yourself with: ``fw=url-to-your-router``'s-status-page ``fw-skip=any-string-preceding-your-IP-address`` + * Need support for another router/firewall? + Define the router yourself with: - ddclient does something like this to provide builtin support for - common routers. - For example, the Linksys routers could have been added with: + ``` + usev4=fwv4 + fwv4=url-to-your-router-status-page + fwv4-skip="regular expression matching any string preceding your IP address, if necessary" + ``` - fw=192.168.1.1/Status.htm - fw-skip=WAN.*?IP Address + ddclient does something like this to provide builtin support for common + routers. + For example, the Linksys routers could have been added with: -OR - Send me the output from: - ``$ ddclient --geturl {fw-ip-status-url} [--login login [--password password]]`` - and I'll add it to the next release! + ``` + usev4=fwv4 + fwv4=192.168.1.1/Status.htm + fwv4-skip=WAN.*?IP Address + ``` -ie. for my fw/router I used: ``$ ddclient --geturl 192.168.1.254/status.htm`` + OR [create a new issue](https://github.com/ddclient/ddclient/issues/new) + containing the output from: - 5. Some broadband routers require the use of a password when ddclient - accesses its status page to determine the router's WAN IP address. - If this is the case for your router, add + ``` + curl --include --location http://url.of.your.firewall/ip-status-page + ``` + so that we can add a new firewall definition to a future release of + ddclient. + + * Some broadband routers require the use of a password when ddclient accesses + its status page to determine the router's WAN IP address. + If this is the case for your router, add + + ``` fw-login=your-router-login fw-password=your-router-password + ``` -to the beginning of your ddclient.conf file. -Note that some routers use either 'root' or 'admin' as their login -while some others accept anything. + to the beginning of your ddclient.conf file. + Note that some routers use either 'root' or 'admin' as their login while + some others accept anything. ## USING DDCLIENT WITH `ppp` diff --git a/ddclient.conf.in b/ddclient.conf.in index 8a336ac..81cc746 100644 --- a/ddclient.conf.in +++ b/ddclient.conf.in @@ -81,26 +81,6 @@ pid=@runstatedir@/ddclient.pid # record PID in file. # protocol=dyndns2 \ # your-dynamic-host.dyndns.org -## -## dyndns.org static addresses -## -## (supports variables: wildcard,mx,backupmx) -## -# static=yes, \ -# server=members.dyndns.org, \ -# protocol=dyndns2 \ -# your-static-host.dyndns.org - -## -## dyndns.org custom addresses -## -## (supports variables: wildcard,mx,backupmx) -## -# custom=yes, \ -# server=members.dyndns.org, \ -# protocol=dyndns2 \ -# your-domain.top-level,your-other-domain.top-level - ## ## ZoneEdit (zoneedit.com) ## @@ -424,3 +404,11 @@ pid=@runstatedir@/ddclient.pid # record PID in file. # login=subdomain.domain.tld \ # password=your_password \ # subdomain.domain.tld + +## +## INWX +## +# protocol=inwx \ +# login=my-inwx-DynDNS-account-username \ +# password=my-inwx-DynDNS-account-password \ +# myhost.example.org diff --git a/ddclient.in b/ddclient.in index adabdb6..78fdf7f 100755 --- a/ddclient.in +++ b/ddclient.in @@ -618,7 +618,6 @@ our %variables = ( 'quiet' => setv(T_BOOL, 0, 0, 0, undef), 'help' => setv(T_BOOL, 0, 0, 0, undef), 'test' => setv(T_BOOL, 0, 0, 0, undef), - 'geturl' => setv(T_STRING,0, 0, undef, undef), 'postscript' => setv(T_POSTS, 0, 0, undef, undef), 'ssl_ca_dir' => setv(T_FILE, 0, 0, undef, undef), @@ -709,7 +708,6 @@ our %variables = ( 'dyndns-common-defaults' => { 'backupmx' => setv(T_BOOL, 0, 1, 0, undef), 'mx' => setv(T_OFQDN, 0, 1, undef, undef), - 'static' => setv(T_BOOL, 0, 1, 0, undef), 'wildcard' => setv(T_BOOL, 0, 1, 0, undef), }, ); @@ -848,6 +846,7 @@ our %protocols = ( 'variables' => { %{$variables{'protocol-common-defaults'}}, %{$variables{'dyndns-common-defaults'}}, + 'static' => setv(T_BOOL, 0, 1, 0, undef), }, }, 'dyndns2' => { @@ -857,7 +856,6 @@ our %protocols = ( 'variables' => { %{$variables{'protocol-common-defaults'}}, %{$variables{'dyndns-common-defaults'}}, - 'custom' => setv(T_BOOL, 0, 1, 0, undef), 'script' => setv(T_STRING, 0, 1, '/nic/update', undef), }, }, @@ -956,6 +954,16 @@ our %protocols = ( 'zone' => setv(T_FQDN, 1, 0, undef, undef), }, }, + 'inwx' => { + 'force_update' => undef, + 'update' => \&nic_inwx_update, + 'examples' => \&nic_inwx_examples, + 'variables' => { + %{$variables{'protocol-common-defaults'}}, + 'server' => setv(T_FQDNP, 0, 0, 'dyndns.inwx.com', undef), + 'script' => setv(T_STRING, 0, 0, '/nic/update', undef), + }, + }, 'mythicdyn' => { 'force_update' => undef, 'update' => \&nic_mythicdyn_update, @@ -1294,7 +1302,6 @@ my @opt = ( ["fw-banlocal", "!", ""], ## deprecated ["if-skip", "=s", ""], ## deprecated ["test", "!", ""], ## hidden - ["geturl", "=s", ""], ## hidden ["redirect", "=i", "--redirect= : enable and follow at most HTTP 30x redirections"], "", nic_examples(), @@ -1306,9 +1313,6 @@ sub main { $saved_recap = ''; %saved_opt = %opt; $result = 'OK'; - - test_geturl(opt('geturl')) if opt('geturl'); - if (opt('help')) { printf "%s\n", $opt_usage; $opt{'version'}('', ''); @@ -2263,22 +2267,7 @@ sub test_possible_ip { exit 0 unless opt('debug'); } -###################################################################### -## test_geturl - print (and save if -test) result of fetching a URL -###################################################################### -sub test_geturl { - my $url = shift; - my $reply = geturl( - proxy => opt('proxy'), - url => $url, - login => opt('login'), - password => opt('password'), - ); - print "URL $url\n"; - print $reply // "\n"; - exit; -} ###################################################################### ## load_file ###################################################################### @@ -2864,7 +2853,7 @@ sub geturl { push(@curlopt, "url=\"".escape_curl_param("${protocol}://${server}/${url}").'"'); # Each header line is added individually - @header_lines = split('\n', $headers); + @header_lines = ref($headers) eq 'ARRAY' ? @$headers : split('\n', $headers); $_ = "header=\"".escape_curl_param($_).'"' for (@header_lines); push(@curlopt, @header_lines); @@ -4015,8 +4004,6 @@ Configuration variables applicable to the 'dyndns2' protocol are: server=fqdn.of.service ## defaults to members.dyndns.org script=/path/to/script ## defaults to /nic/update backupmx=no|yes ## indicates that this host is the primary MX for the domain. - static=no|yes ## indicates that this host has a static IP address. - custom=no|yes ## indicates that this host is a 'custom' top-level domain name. mx=any.host.domain ## a host MX'ing for this host definition. wildcard=no|yes ## add a DNS wildcard CNAME record that points to login=service-login ## login name and password registered with the service @@ -4050,7 +4037,6 @@ EoEXAMPLE ###################################################################### sub nic_dyndns2_update { debug("\nnic_dyndns2_update -------------------"); - my @groups = group_hosts_by(\@_, qw(login password server script static custom wildcard mx backupmx wantipv4 wantipv6)); my %errors = ( 'badauth' => 'Bad authorization (username or password)', 'badsys' => 'The system parameter given was not valid', @@ -4064,7 +4050,18 @@ sub nic_dyndns2_update { 'dnserr' => 'System error: DNS error encountered. Contact support@dyndns.org', 'nochg' => 'No update required; unnecessary attempts to change to the current address are considered abusive', ); - for my $group (@groups) { + my @group_by_attrs = qw( + backupmx + login + mx + password + script + server + wantipv4 + wantipv6 + wildcard + ); + for my $group (group_hosts_by(\@_, @group_by_attrs)) { my @hosts = @{$group->{hosts}}; my %groupcfg = %{$group->{cfg}}; my $hosts = join(',', @hosts); @@ -4072,21 +4069,9 @@ sub nic_dyndns2_update { my $ipv6 = $groupcfg{'wantipv6'}; delete $config{$_}{'wantipv4'} for @hosts; delete $config{$_}{'wantipv6'} for @hosts; - info("setting IPv4 address to %s for %s", $ipv4, $hosts) if $ipv4; - info("setting IPv6 address to %s for %s", $ipv6, $hosts) if $ipv6; - verbose("UPDATE:", "updating %s", $hosts); - my $url = "$groupcfg{'server'}$groupcfg{'script'}?system="; - if ($groupcfg{'custom'}) { - warning("updating %s: 'custom' and 'static' may not be used together. ('static' ignored)", $hosts) - if $groupcfg{'static'}; - $url .= 'custom'; - } elsif ($groupcfg{'static'}) { - $url .= 'statdns'; - } else { - $url .= 'dyndns'; - } - $url .= "&hostname=$hosts"; - $url .= "&myip="; + info("$hosts: setting IPv4 address to $ipv4") if $ipv4; + info("$hosts: setting IPv6 address to $ipv6") if $ipv6; + my $url = "$groupcfg{'server'}$groupcfg{'script'}?hostname=$hosts&myip="; $url .= $ipv4 if $ipv4; if ($ipv6) { $url .= "," if $ipv4; @@ -4105,65 +4090,64 @@ sub nic_dyndns2_update { password => $groupcfg{'password'}, ) // ''; if ($reply eq '') { - failed("updating %s: Could not connect to %s.", $hosts, $groupcfg{'server'}); + failed("$hosts: Could not connect to $groupcfg{'server'}"); next; } next if !header_ok($hosts, $reply); - my @reply = split /\n/, $reply; - my $state = 'header'; - for my $line (@reply) { - if ($state eq 'header') { - $state = 'body'; - } elsif ($state eq 'body') { - $state = 'results' if $line eq ''; - } elsif ($state =~ /^results/) { - $state = 'results2'; - # bug #10: some dyndns providers does not return the IP so - # we can't use the returned IP - my ($status, $returnedips) = split / /, lc $line; - for my $h (@hosts) { - $config{$h}{'status-ipv4'} = $status if $ipv4; - $config{$h}{'status-ipv6'} = $status if $ipv6; - } - if ($status eq 'good') { - for my $h (@hosts) { - $config{$h}{'ipv4'} = $ipv4 if $ipv4; - $config{$h}{'ipv6'} = $ipv6 if $ipv6; - $config{$h}{'mtime'} = $now; - } - success("updating %s: %s: IPv4 address set to %s", $hosts, $status, $ipv4) if $ipv4; - success("updating %s: %s: IPv6 address set to %s", $hosts, $status, $ipv6) if $ipv6; - } elsif (exists $errors{$status}) { - if ($status eq 'nochg') { - warning("updating %s: %s: %s", $hosts, $status, $errors{$status}); - for my $h (@hosts) { - $config{$h}{'ipv4'} = $ipv4 if $ipv4; - $config{$h}{'ipv6'} = $ipv6 if $ipv6; - $config{$h}{'mtime'} = $now; - $config{$h}{'status-ipv4'} = 'good' if $ipv4; - $config{$h}{'status-ipv6'} = 'good' if $ipv6; - } - } else { - failed("updating %s: %s: %s", $hosts, $status, $errors{$status}); - } - } elsif ($status =~ /w(\d+)(.)/) { - my ($wait, $units) = ($1, lc $2); - my ($sec, $scale) = ($wait, 1); - ($scale, $units) = (1, 'seconds') if $units eq 's'; - ($scale, $units) = (60, 'minutes') if $units eq 'm'; - ($scale, $units) = (60*60, 'hours') if $units eq 'h'; - $sec = $wait * $scale; - for my $h (@hosts) { - $config{$h}{'wtime'} = $now + $sec; - } - warning("updating %s: %s: wait %s %s before further updates", $hosts, $status, $wait, $units); - } else { - failed("updating %s: unexpected status (%s)", $hosts, $line); - } - } + # Some services can return 200 OK even if there is an error (e.g., bad authentication, + # updates too frequent) so the body of the response must also be checked. + (my $body = $reply) =~ s/^.*?\n\n//s; + my @reply = split(qr/\n/, $body); + if (!@reply) { + failed("$hosts: Could not connect to $groupcfg{'server'}"); + next; + } + # From : + # + # If updating multiple hostnames, hostname-specific return codes are given one per line, + # in the same order as the hostnames were specified. Return codes indicating a failure + # with the account or the system are given only once. + # + # TODO: There is no mention of what happens if multiple IP addresses are supplied (e.g., + # IPv4 and IPv6) for a host. If one address fails to update and the other doesn't, is that + # one error status line? An error status line and a success status line? Or is an update + # considered to be all-or-nothing and the status applies to the operation as a whole? If + # the IPv4 address changes but not the IPv6 address does that result in a status of "good" + # because the set of addresses for a host changed even if a subset did not? + # + # TODO: The logic below applies the last line's status to all hosts. Change it to apply + # each status to its corresponding host. + for my $line (@reply) { + # The IP address normally comes after the status, but we ignore it. We could compare + # it with the expected address and mark the update as failed if it differs, but (1) + # some services do not return the IP; and (2) comparison is brittle (e.g., + # 192.000.002.001 vs. 192.0.2.1) and false errors could cause high load on the service + # (an update attempt every min-error-interval instead of every max-interval). + (my $status = $line) =~ s/ .*$//; + if ($status eq 'nochg') { + warning("$hosts: $status: $errors{$status}"); + $status = 'good'; + } + for my $h (@hosts) { + $config{$h}{'status-ipv4'} = $status if $ipv4; + $config{$h}{'status-ipv6'} = $status if $ipv6; + } + if ($status ne 'good') { + if (exists($errors{$status})) { + failed("$hosts: $status: $errors{$status}"); + } else { + failed("$hosts: unexpected status: $line"); + } + next; + } + for my $h (@hosts) { + $config{$h}{'ipv4'} = $ipv4 if $ipv4; + $config{$h}{'ipv6'} = $ipv6 if $ipv6; + $config{$h}{'mtime'} = $now; + } + success("$hosts: IPv4 address set to $ipv4") if $ipv4; + success("$hosts: IPv6 address set to $ipv6") if $ipv6; } - failed("updating %s: Could not connect to %s.", $hosts, $groupcfg{'server'}) - if $state ne 'results2'; } } @@ -5733,92 +5717,75 @@ EoEXAMPLE ###################################################################### sub nic_godaddy_update { debug("\nnic_godaddy_update --------------------"); - for my $host (@_) { - my $ipv4 = delete $config{$host}{'wantipv4'}; - my $ipv6 = delete $config{$host}{'wantipv6'}; - - my $zone = $config{$host}{'zone'}; - (my $hostname = $host) =~ s/\.\Q$zone\E$//; - - for my $ip ($ipv4, $ipv6) { - next if (!$ip); - - info("%s.%s -- Setting IP address to %s.", $hostname, $zone, $ip); - verbose("UPDATE:", "updating %s.%s", $hostname, $zone); - - my $ipversion = ($ip eq ($ipv6 // '')) ? '6' : '4'; - my $status = \$config{$host}{"status-ipv$ipversion"}; - my $rrset_type = ($ipversion eq '6') ? 'AAAA' : 'A'; - my $data = encode_json([ { - data => $ip, - defined($config{$host}{'ttl'}) ? (ttl => $config{$host}{'ttl'}) : (), - name => $hostname, - type => $rrset_type, - } ]); - - my $url = "https://$config{$host}{'server'}"; - $url .= "/${zone}/records/${rrset_type}/${hostname}"; - - my $header = "Content-Type: application/json\n"; - $header .= "Accept: application/json\n"; - $header .= "Authorization: sso-key $config{$host}{'login'}:$config{$host}{'password'}\n"; + for my $h (@_) { + my $zone = $config{$h}{'zone'}; + (my $hostname = $h) =~ s/\.\Q$zone\E$//; + for my $ipv ('4', '6') { + my $ip = delete($config{$h}{"wantipv$ipv"}) or next; + info("$h: Setting IPv$ipv address to $ip"); + my $rrset_type = ($ipv eq '6') ? 'AAAA' : 'A'; + my $url = "https://$config{$h}{'server'}/$zone/records/$rrset_type/$hostname"; my $reply = geturl( proxy => opt('proxy'), url => $url, - headers => $header, + headers => [ + 'Content-Type: application/json', + 'Accept: application/json', + "Authorization: sso-key $config{$h}{'login'}:$config{$h}{'password'}", + ], method => 'PUT', - data => $data, + data => encode_json([{ + data => $ip, + defined($config{$h}{'ttl'}) ? (ttl => $config{$h}{'ttl'}) : (), + name => $hostname, + type => $rrset_type, + }]), ); unless ($reply) { - failed("%s.%s -- Could not connect to %s.", $hostname, $zone, $config{$host}{'server'}); + failed("$h: Could not connect to $config{$h}{'server'}"); next; } - (my $code) = ($reply =~ m%^s*HTTP/.*\s+(\d+)%i); - my $ok = header_ok($host, $reply); - my $msg; - $reply =~ s/^.*?\n\n//s; # extract payload + my $ok = header_ok($h, $reply); + $reply =~ s/^.*?\n\n//s; my $response = eval {decode_json($reply)}; - if (!defined($response) && $code != "200") { - $$status = "bad"; - - failed("%s.%s -- Unexpected or empty service response, cannot parse data.", $hostname, $zone); - } elsif (defined($response->{code})) { - info("%s.%s -- %s - %s.", $hostname, $zone, $response->{code}, $response->{message}); - } - if ($ok) { - # read data - $config{$host}{"ipv$ipversion"} = $ip; - $config{$host}{'mtime'} = $now; - $$status = 'good'; - - success("%s.%s -- Updated successfully to %s (status: %s).", $hostname, $zone, $ip, $code); + if (!defined($response)) { + failed("$h: Unexpected or empty service response, cannot parse data"); next; - } elsif ($code == "400") { - $msg = 'GoDaddy API URL ($url) was malformed.'; - } elsif ($code == "401") { # authentication error - if ($config{$host}{'login'} && $config{$host}{'login'}) { - $msg = 'login or password option incorrect.'; - } else { - $msg = 'login or password option missing.'; - } - $msg .= ' Correct values can be obtained from from https://developer.godaddy.com/keys/.'; - } elsif ($code == "403") { - $msg = 'Customer identified by login and password options denied permission.'; - } elsif ($code == "404") { - $msg = "\"${hostname}.${zone}\" not found at GoDaddy, please check zone option and login/password."; - } elsif ($code == "422") { - $msg = "\"${hostname}.${zone}\" has invalid domain or lacks A/AAAA record."; - } elsif ($code == "429") { - $msg = 'Too many requests to GoDaddy within brief period.'; - } elsif ($code == "503") { - $msg = "\"${hostname}.${zone}\" is unavailable."; - } else { - $msg = 'Unexpected service response.'; + } elsif (defined($response->{code})) { + info("$h: $response->{code} - $response->{message}"); } - - $$status = 'bad'; - failed("%s.%s -- %s", $hostname, $zone, $msg); + if (!$ok) { + my $msg; + if ($code eq "400") { + $msg = 'GoDaddy API URL ($url) was malformed.'; + } elsif ($code eq "401") { + if ($config{$h}{'login'} && $config{$h}{'login'}) { + $msg = 'login or password option incorrect.'; + } else { + $msg = 'login or password option missing.'; + } + $msg .= ' Correct values can be obtained from from https://developer.godaddy.com/keys/.'; + } elsif ($code eq "403") { + $msg = 'Customer identified by login and password options denied permission.'; + } elsif ($code eq "404") { + $msg = "\"$h\" not found at GoDaddy, please check zone option and login/password."; + } elsif ($code eq "422") { + $msg = "\"$h\" has invalid domain or lacks A/AAAA record."; + } elsif ($code eq "429") { + $msg = 'Too many requests to GoDaddy within brief period.'; + } elsif ($code eq "503") { + $msg = "\"$h\" is unavailable."; + } else { + $msg = 'Unexpected service response.'; + } + failed("$h: $msg"); + next; + } + $config{$h}{"ipv$ipv"} = $ip; + $config{$h}{'mtime'} = $now; + $config{$h}{"status-ipv$ipv"} = 'good'; + success("$h: Updated successfully to $ip (status: $code)"); } } } @@ -6472,6 +6439,167 @@ sub nic_hetzner_update { } } +###################################################################### +## nic_inwx_examples +###################################################################### +sub nic_inwx_examples { + return <<"EoEXAMPLE"; +o 'inwx' + +The 'inwx' protocol is designed for DynDNS accounts at INWX +. It is similar to the 'dyndns2' protocol except IPv6 +addresses are passed in a separate 'myipv6' URL parameter (rather than included +in the 'myip' parameter): + + https://dyndns.inwx.com/nic/update?myip=&myipv6= + +The 'inwx' protocol was designed around INWX's behavior as of June 2024: + - Omitting the IPv4 address (either no 'myip' URL parameter or '' is + the empty string) will cause INWX to silently set the IPv4 address (A + record) to '127.0.0.1'. No error message is returned. + - Omitting the IPv6 address (either no 'myipv6' URL parameter or '' + is the empty string) will cause INWX to delete the IPv6 address (AAAA + record) if it exists. + - INWX will automatically create an IPv6 AAAA record for your hostname if + necessary. + - 'dyndns.inwx.com' is not reachable via IPv6 (there is no AAAA record). + - GET 'https://dyndns.inwx.com/nic/update' without further parameters will set + the IPv4 A record to the public IP of the requesting host and delete the + IPv6 AAAA record. + - You can ask INWX support to manually convert a DynDNS account into an + IPv6-only account. No A record will be created in that case. + +Configuration variables applicable to the 'inwx' protocol are: + protocol=inwx ## + server=fqdn.of.service ## defaults to dyndns.inwx.com + script=/path/to/script ## defaults to /nic/update + login=service-login ## login name and password registered with the service + password=service-password ## + fully.qualified.host ## the host registered with the service. + +Example ${program}.conf file entries: + ## single host update + protocol=inwx \\ + login=my-inwx-DynDNS-account-username \\ + password=my-inwx-DynDNS-account-password \\ + myhost.example.org +EoEXAMPLE +} + +###################################################################### +## nic_inwx_update +###################################################################### +sub nic_inwx_update { + debug("\nnic_inwx_update -------------------"); + my %errors = ( + 'badauth' => 'Bad authorization (username or password)', + 'badsys' => 'The system parameter given was not valid', + 'notfqdn' => 'A Fully-Qualified Domain Name was not provided', + 'nohost' => 'The hostname specified does not exist in the database', + '!yours' => 'The hostname specified exists, but not under the username currently being used', + '!donator' => 'The offline setting was set, when the user is not a donator', + '!active' => 'The hostname specified is in a Custom DNS domain which has not yet been activated.', + 'abuse' => 'The hostname specified is blocked for abuse; you should receive an email notification which provides an unblock request link.', + 'numhost' => 'System error: Too many or too few hosts found.', + 'dnserr' => 'System error: DNS error encountered.', + 'nochg' => 'No update required; unnecessary attempts to change to the current address are considered abusive', + ); + my @group_by_attrs = qw( + login + password + server + script + wantipv4 + wantipv6 + ); + for my $group (group_hosts_by(\@_, @group_by_attrs)) { + my @hosts = @{$group->{hosts}}; + my %groupcfg = %{$group->{cfg}}; + my $hosts = join(',', @hosts); + my $ipv4 = $groupcfg{'wantipv4'}; + my $ipv6 = $groupcfg{'wantipv6'}; + delete $config{$_}{'wantipv4'} for @hosts; + delete $config{$_}{'wantipv6'} for @hosts; + info("$hosts: setting IPv4 address to $ipv4") if $ipv4; + info("$hosts: setting IPv6 address to $ipv6") if $ipv6; + my $url = "$groupcfg{'server'}$groupcfg{'script'}?"; + $url .= "myip=$ipv4" if $ipv4; + if ($ipv6) { + if (!$ipv4 && opt('usev4', $hosts) ne 'disabled') { + warning("Skipping IPv6 AAAA record update because INWX requires the IPv4 A record to be updated at the same time but the IPv4 address is unknown."); + next; + } + $url .= "&" if $ipv4; + $url .= "myipv6=$ipv6"; + } + my $reply = geturl( + proxy => opt('proxy'), + url => $url, + login => $groupcfg{'login'}, + password => $groupcfg{'password'}, + ) // ''; + if ($reply eq '') { + failed("$hosts: Could not connect to $groupcfg{'server'}"); + next; + } + next if !header_ok($hosts, $reply); + # INWX can return 200 OK even if there is an error (e.g., bad authentication, + # updates too frequent) so the body of the response must also be checked. + (my $body = $reply) =~ s/^.*?\n\n//s; + my @reply = split(qr/\n/, $body); + if (!@reply) { + failed("$hosts: Could not connect to $groupcfg{'server'}"); + next; + } + # From : + # + # If updating multiple hostnames, hostname-specific return codes are given one per line, + # in the same order as the hostnames were specified. Return codes indicating a failure + # with the account or the system are given only once. + # + # TODO: There is no mention of what happens if multiple IP addresses are supplied (e.g., + # IPv4 and IPv6) for a host. If one address fails to update and the other doesn't, is that + # one error status line? An error status line and a success status line? Or is an update + # considered to be all-or-nothing and the status applies to the operation as a whole? If + # the IPv4 address changes but not the IPv6 address does that result in a status of "good" + # because the set of addresses for a host changed even if a subset did not? + # + # TODO: The logic below applies the last line's status to all hosts. Change it to apply + # each status to its corresponding host. + for my $line (@reply) { + # The IP address normally comes after the status, but we ignore it. We could compare + # it with the expected address and mark the update as failed if it differs, but (1) + # some services do not return the IP; and (2) comparison is brittle (e.g., + # 192.000.002.001 vs. 192.0.2.1) and false errors could cause high load on the service + # (an update attempt every min-error-interval instead of every max-interval). + (my $status = $line) =~ s/ .*$//; + if ($status eq 'nochg') { + warning("$hosts: $status: $errors{$status}"); + $status = 'good'; + } + for my $h (@hosts) { + $config{$h}{'status-ipv4'} = $status if $ipv4; + $config{$h}{'status-ipv6'} = $status if $ipv6; + } + if ($status ne 'good') { + if (exists($errors{$status})) { + failed("$hosts: $status: $errors{$status}"); + } else { + failed("$hosts: unexpected status: $line"); + } + next; + } + for my $h (@hosts) { + $config{$h}{'ipv4'} = $ipv4 if $ipv4; + $config{$h}{'ipv6'} = $ipv6 if $ipv6; + $config{$h}{'mtime'} = $now; + } + success("$hosts: IPv4 address set to $ipv4") if $ipv4; + success("$hosts: IPv6 address set to $ipv6") if $ipv6; + } + } +} + ###################################################################### ## nic_yandex_examples ######################################################################