Merge branch 'ddclient:master' into master

This commit is contained in:
philippderdiedas 2024-07-20 00:34:59 +02:00 committed by GitHub
commit 44f96cb1a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 357 additions and 214 deletions

View file

@ -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

View file

@ -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`

View file

@ -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

View file

@ -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=<max> : enable and follow at most <max> 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 // "<undefined>\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 <host>
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 <https://help.dyn.com/remote-access-api/return-codes/>:
#
# 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
<https://www.inwx.com/>. 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=<ipaddr>&myipv6=<ip6addr>
The 'inwx' protocol was designed around INWX's behavior as of June 2024:
- Omitting the IPv4 address (either no 'myip' URL parameter or '<ipaddr>' 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 '<ip6addr>'
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 <https://help.dyn.com/remote-access-api/return-codes/>:
#
# 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
######################################################################