diff --git a/ChangeLog.md b/ChangeLog.md index 881c76a..f3919f2 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,7 +3,31 @@ This document describes notable changes. For details, see the [source code repository history](https://github.com/ddclient/ddclient/commits/master). +## 2023-XX-XX v3.11.0 + +### Breaking changes + + * ddclient no longer ships any example files for init systems that use `/etc/init.d`. + This was done because those files where effectively unmaintained, untested by the developers and only updated by downstream distros. + If you where relying on those files, please copy them into your packaging. + +### New features + + * Added support for domaindiscount24.com + * Added support for domeneshop.no + * Added support for Enom + * Added support for Mythic Beasts Dynamic DNS + * Added support for njal.la + * Added support for Porkbun + * Added support for IPv6 to the EasyDNS and DuckDNS provider + +### Bug fixes + + * DynDNS2 now uses the newer ipv4/ipv6 syntax's + * The OVH provider now ignores extra data returned + ## 2022-10-20 v3.10.0 + ### New features * Added support for domaindiscount24.com @@ -12,6 +36,7 @@ repository history](https://github.com/ddclient/ddclient/commits/master). ## 2022-05-15 v3.10.0_2 ### Bug fixes + * Fix version number being unable to parse ## 2022-05-15 v3.10.0_1 diff --git a/Makefile.am b/Makefile.am index b829573..a9543ee 100644 --- a/Makefile.am +++ b/Makefile.am @@ -13,12 +13,6 @@ EXTRA_DIST = \ sample-etc_dhclient-exit-hooks \ sample-etc_dhcpc_dhcpcd-eth0.exe \ sample-etc_ppp_ip-up.local \ - sample-etc_rc.d_ddclient.freebsd \ - sample-etc_rc.d_init.d_ddclient \ - sample-etc_rc.d_init.d_ddclient.alpine \ - sample-etc_rc.d_init.d_ddclient.lsb \ - sample-etc_rc.d_init.d_ddclient.redhat \ - sample-etc_rc.d_init.d_ddclient.ubuntu \ sample-etc_systemd.service \ sample-get-ip-from-fritzbox CLEANFILES = diff --git a/README.md b/README.md index 2f97385..05c8abf 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Dynamic DNS services currently supported include: DonDominio - See https://www.dondominio.com for details NearlyFreeSpeech.net - See https://www.nearlyfreespeech.net/services/dns for details OVH - See https://www.ovh.com for details + Porkbun - See https://porkbun.com/ ClouDNS - See https://www.cloudns.net dinahosting - See https://dinahosting.com Gandi - See https://gandi.net @@ -36,6 +37,9 @@ Dynamic DNS services currently supported include: 1984.is - See https://www.1984.is/product/freedns/ for details Njal.la - See https://njal.la/docs/ddns/ regfish.de - See https://www.regfish.de/domains/dyndns/ for details + domenehsop - See https://api.domeneshop.no/docs/#tag/ddns/paths/~1dyndns~1update/get + Mythic Beasts - See https://www.mythic-beasts.com/support/api/dnsv2/dynamic-dns for details + Enom - See https://www.enom.com for details cdmon.org - See https://ticket.cdmon.com/ca/support/solutions/articles/7000005922-api-d-actualitzaci%C3%B3-de-ip-del-dns-gratis-din%C3%A0mic `ddclient` now supports many cable and DSL broadband routers. @@ -113,82 +117,6 @@ start the first time by hand systemctl start ddclient.service -#### Redhat style rc files and daemon-mode - - cp sample-etc_rc.d_init.d_ddclient /etc/rc.d/init.d/ddclient - -enable automatic startup when booting. also check your distribution - - /sbin/chkconfig --add ddclient - -start the first time by hand - - /etc/rc.d/init.d/ddclient start - -#### Alpine style rc files and daemon-mode - - cp sample-etc_rc.d_init.d_ddclient.alpine /etc/init.d/ddclient - -enable automatic startup when booting - - rc-update add ddclient - -make sure you have perl installed - - apk add perl - -start the first time by hand - - rc-service ddclient start - -#### Ubuntu style rc files and daemon-mode - - cp sample-etc_rc.d_init.d_ddclient.ubuntu /etc/init.d/ddclient - -enable automatic startup when booting - - update-rc.d ddclient defaults - -make sure you have perl and the required modules installed - - apt-get install perl libdata-validate-ip-perl libio-socket-ssl-perl - -if you plan to use cloudflare or feedns you need the perl json module - - apt-get install libjson-pp-perl - -for IPv6 you also need to instal the perl io-socket-inet6 module - - apt install libio-socket-inet6-perl - -start the first time by hand - - service ddclient start - -#### FreeBSD style rc files and daemon mode - - mkdir -p /usr/local/etc/rc.d - cp sample-etc_rc.d_ddclient.freebsd /usr/local/etc/rc.d/ddclient - -enable automatic startup when booting - - sysrc ddclient_enable=YES - -make sure you have perl and the required modules installed - - pkg install perl5 p5-Data-Validate-IP p5-IO-Socket-SSL - -if you plan to use cloudflare or feedns you need the perl json module - - pkg install p5-JSON-PP - -start the service manually for the first time - - service ddclient start - - -If you are not using daemon-mode, configure cron and dhcp or ppp as described below. - ## TROUBLESHOOTING 1. enable debugging and verbose messages: ``$ ddclient -daemon=0 -debug -verbose -noquiet`` @@ -248,7 +176,7 @@ not become stale. cp sample-etc_cron.d_ddclient /etc/cron.d/ddclient vi /etc/cron.d/ddclient -## USING DDCLIENT WITH `dhcpcd-1.3.17` +## USING DDCLIENT WITH `dhcpcd` If you are using dhcpcd-1.3.17 or thereabouts, you can easily update your DynDNS entry automatically every time your lease is obtained diff --git a/README.ssl b/README.ssl deleted file mode 100644 index 3aa579b..0000000 --- a/README.ssl +++ /dev/null @@ -1,13 +0,0 @@ -Since 3.7.0, ddclient support ssl-updates -To use ssl, put "ssl=yes" in your configuration and make sure -you have IO::Socket::SSL. - -On debian, you need libio-socket-ssl-perl to have IO::Socket::SSL - -On alpine, you need perl-io-socket-ssl to have IO::Socket::SSL - -ssl support is tested on folowing dynamic dns providers: -- dyndns.com -- freemyip.com -- DNS Made Easy -- dondominio.com diff --git a/configure.ac b/configure.ac index 6c58b8f..8a550d0 100644 --- a/configure.ac +++ b/configure.ac @@ -1,5 +1,5 @@ AC_PREREQ([2.63]) -AC_INIT([ddclient], [3.10.1]) +AC_INIT([ddclient], [3.11.0]) AC_CONFIG_SRCDIR([ddclient.in]) AC_CONFIG_AUX_DIR([build-aux]) AC_CONFIG_MACRO_DIR([m4]) diff --git a/ddclient.conf.in b/ddclient.conf.in index 6dca259..36dd49f 100644 --- a/ddclient.conf.in +++ b/ddclient.conf.in @@ -285,6 +285,15 @@ ssl=yes # use ssl-support. Works with # password=your_password # test.example.com +## +## Porkbun (https://porkbun.com/) +## +# protocol=porkbun +# apikey=APIKey +# secretapikey=SecretAPIKey +# host.example.com,host2.sub.example.com +# on-root-domain=yes example.com,sub.example.com + ## ## ClouDNS (https://www.cloudns.net) ## @@ -308,6 +317,14 @@ ssl=yes # use ssl-support. Works with #password=mypassword, \ #subdomain-1.domain.com,subdomain-2.domain.com +## +## domeneshop (www.domeneshop.no) +## +# protocol=domeneshop +# login= +# password= +# subdomain-1.domain.com,subdomain-2.domain.com + ## ## Njal.la (http://njal.la/) ## @@ -323,6 +340,15 @@ ssl=yes # use ssl-support. Works with # password=mypassword # my-domain.com +## +## Enom (www.enom.com) +## +# protocol=enom, +# login=domain.name, +# password=domain-password +# my-domain.com + + ## ## cdmon.org (https://dinamico.cdmon.org/) ## diff --git a/ddclient.in b/ddclient.in index c2d2983..3b1f9a9 100755 --- a/ddclient.in +++ b/ddclient.in @@ -490,17 +490,32 @@ my %variables = ( 'host' => setv(T_STRING,1, 1, '', 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), '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,0, 0, '', undef), 'web-ssl-validate' => setv(T_BOOL, 0, 0, 1, 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,0, 0, '', undef), 'fw-login' => setv(T_LOGIN, 0, 0, '', undef), 'fw-password' => setv(T_PASSWD,0, 0, '', undef), 'fw-ssl-validate' => setv(T_BOOL, 0, 0, 1, undef), + '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), 'cmd' => setv(T_PROG, 0, 0, '', undef), 'cmd-skip' => setv(T_STRING,0, 0, '', undef), + 'cmdv4' => setv(T_PROG, 0, 0, '', undef), + 'cmdv6' => setv(T_PROG, 0, 0, '', 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), @@ -639,6 +654,15 @@ my %services = ( 'host' => setv(T_NUMBER, 1, 1, 0, undef), }, }, + 'domeneshop' => { + 'updateable' => undef, + 'update' => \&nic_domeneshop_update, + 'examples' => \&nic_domeneshop_examples, + 'variables' => { + %{$variables{'service-common-defaults'}}, + 'server' => setv(T_FQDNP, 1, 0, 'api.domeneshop.no', undef), + }, + }, 'duckdns' => { 'updateable' => undef, 'update' => \&nic_duckdns_update, @@ -753,6 +777,16 @@ my %services = ( 'zone' => setv(T_FQDN, 1, 0, '', undef), }, }, + 'mythicdyn' => { + 'updateable' => undef, + 'update' => \&nic_mythicdyn_update, + 'examples' => \&nic_mythicdyn_examples, + 'variables' => { + %{$variables{'service-common-defaults'}}, + 'min-interval' => setv(T_DELAY, 0, 0, interval('5m'), 0), + 'server' => setv(T_FQDNP, 1, 0, 'api.mythic-beasts.com', undef), + }, + }, 'namecheap' => { 'updateable' => undef, 'update' => \&nic_namecheap_update, @@ -833,6 +867,21 @@ my %services = ( 'server' => setv(T_FQDNP, 1, 0, 'www.ovh.com', undef), }, }, + 'porkbun' => { + 'updateable' => undef, + 'update' => \&nic_porkbun_update, + 'examples' => \&nic_porkbun_examples, + 'variables' => { + 'apikey' => setv(T_PASSWD, 1, 0, '', undef), + 'secretapikey' => setv(T_PASSWD, 1, 0, '', undef), + 'on-root-domain' => setv(T_BOOL, 0, 0, 0, undef), + 'login' => setv(T_LOGIN, 0, 0, 'unused', undef), + 'password' => setv(T_PASSWD, 0, 0, 'unused', undef), + 'use' => setv(T_USE, 0, 0, 'disabled', undef), + 'usev4' => setv(T_USEV4, 0, 0, 'disabled', undef), + 'usev6' => setv(T_USEV6, 0, 0, 'disabled', undef), + }, + }, 'sitelutions' => { 'updateable' => undef, 'update' => \&nic_sitelutions_update, @@ -917,6 +966,16 @@ my %services = ( $variables{'service-common-defaults'}, ), }, + 'enom' => { + 'updateable' => undef, + 'update' => \&nic_enom_update, + 'examples' => \&nic_enom_examples, + 'variables' => { + %{$variables{'service-common-defaults'}}, + 'server' => setv(T_FQDNP, 1, 0, 'dynamic.name-services.com', undef), + 'min-interval' => setv(T_DELAY, 0, 0, 0, interval('5m')), + }, + }, ); $variables{'merged'} = { map({ %{$services{$_}{'variables'}} } keys(%services)), @@ -1181,6 +1240,7 @@ sub update_nics { my $usev6 = opt('usev6', $h) // 'disabled'; $use = 'disabled' if ($use eq 'no'); # backward compatibility $usev6 = 'disabled' if ($usev6 eq 'no'); # backward compatibility + $use = 'disabled' if ($usev4 ne 'disabled') || ($usev6 ne 'disabled'); my $arg_ip = opt('ip', $h) // ''; my $arg_ipv4 = opt('ipv4', $h) // ''; my $arg_ipv6 = opt('ipv6', $h) // ''; @@ -1739,7 +1799,7 @@ sub init_config { $proto = opt('protocol') if !defined($proto); load_sha1_support($proto) if (grep (/^$proto$/, ("freedns", "nfsn"))); - load_json_support($proto) if (grep (/^$proto$/, ("1984", "cloudflare", "gandi", "godaddy", "hetzner", "yandex", "nfsn", "njalla"))); + load_json_support($proto) if (grep (/^$proto$/, ("1984", "cloudflare", "gandi", "godaddy", "hetzner", "yandex", "nfsn", "njalla", "porkbun"))); if (!exists($services{$proto})) { warning("skipping host: %s: unrecognized protocol '%s'", $h, $proto); @@ -3610,6 +3670,7 @@ sub nic_updateable { my $usev6 = opt('usev6', $host) // 'disabled'; $use = 'disabled' if ($use eq 'no'); # backward compatibility $usev6 = 'disabled' if ($usev6 eq 'no'); # backward compatibility + $use = 'disabled' if ($usev4 ne 'disabled') || ($usev6 ne 'disabled'); # If we have a valid IP address and we have previously warned that it was invalid. # reset the warning count back to zero. @@ -3810,7 +3871,7 @@ sub nic_updateable { 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); + success("%s: skipped: IPv4 address was already set to %s.", $host, $ipv4); } if ($usev6 ne 'disabled') { success("%s: skipped: IPv6 address was already set to %s.", $host, $ipv6); @@ -3852,7 +3913,9 @@ sub header_ok { $ok = 1; } elsif ($result eq '401') { - failed("updating %s: authorization failed (%s)", $host, $line); + failed("updating %s: authentication failed (%s)", $host, $line); + } elsif ($result eq '403') { + failed("updating %s: not authorized (%s)", $host, $line); } } else { @@ -4057,10 +4120,13 @@ sub nic_dyndns2_update { my @hosts = @{$groups{$sig}}; my $hosts = join(',', @hosts); my $h = $hosts[0]; - my $ip = $config{$h}{'wantip'}; - delete $config{$_}{'wantip'} foreach @hosts; + my $ipv4 = $config{$h}{'wantipv4'}; + my $ipv6 = $config{$h}{'wantipv6'}; + delete $config{$_}{'wantipv4'} foreach @hosts; + delete $config{$_}{'wantipv6'} foreach @hosts; - info("setting IP address to %s for %s", $ip, $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); ## Select the DynDNS system to update @@ -4079,7 +4145,11 @@ sub nic_dyndns2_update { $url .= "&hostname=$hosts"; $url .= "&myip="; - $url .= $ip if $ip; + $url .= $ipv4 if $ipv4; + if ($ipv6) { + $url .= "," if $ipv4; + $url .= $ipv6; + } ## some args are not valid for a custom domain. $url .= "&wildcard=ON" if ynu($config{$h}{'wildcard'}, 1, 0, 0); @@ -4102,7 +4172,6 @@ sub nic_dyndns2_update { my @reply = split /\n/, $reply; my $state = 'header'; - my $returnedip = $ip; foreach my $line (@reply) { if ($state eq 'header') { @@ -4116,22 +4185,28 @@ sub nic_dyndns2_update { # bug #10: some dyndns providers does not return the IP so # we can't use the returned IP - my ($status, $returnedip) = split / /, lc $line; - $ip = $returnedip if (not $ip); + my ($status, $returnedips) = split / /, lc $line; my $h = shift @hosts; $config{$h}{'status'} = $status; + $config{$h}{'status-ipv4'} = $status if $ipv4; + $config{$h}{'status-ipv6'} = $status if $ipv6; if ($status eq 'good') { - $config{$h}{'ip'} = $ip; + $config{$h}{'ipv4'} = $ipv4 if $ipv4; + $config{$h}{'ipv6'} = $ipv6 if $ipv6; $config{$h}{'mtime'} = $now; - success("updating %s: %s: IP address set to %s", $h, $status, $ip); + success("updating %s: %s: IPv4 address set to %s", $h, $status, $ipv4) if $ipv4; + success("updating %s: %s: IPv6 address set to %s", $h, $status, $ipv6) if $ipv6; } elsif (exists $errors{$status}) { if ($status eq 'nochg') { warning("updating %s: %s: %s", $h, $status, $errors{$status}); - $config{$h}{'ip'} = $ip; + $config{$h}{'ipv4'} = $ipv4 if $ipv4; + $config{$h}{'ipv6'} = $ipv6 if $ipv6; $config{$h}{'mtime'} = $now; $config{$h}{'status'} = 'good'; + $config{$h}{'status-ipv4'} = 'good' if $ipv4; + $config{$h}{'status-ipv6'} = 'good' if $ipv6; } else { failed("updating %s: %s: %s", $h, $status, $errors{$status}); @@ -4476,6 +4551,86 @@ sub nic_dslreports1_update { } } +###################################################################### +## nic_domeneshop_examples +###################################################################### +sub nic_domeneshop_examples { + return <<"EoEXAMPLE"; +o 'domeneshop' + +API is documented here: https://api.domeneshop.no/docs/ + +To generate credentials, visit https://www.domeneshop.no/admin?view=api after logging in to the control panel at +https://www.domeneshop.no/admin?view=api + +Configuration variables applicable to the 'domeneshop' api are: + protocol=domeneshop ## + login=token ## api-token + password=secret ## api-secret + domain.example.com ## the host registered with the service. ## the host registered with the service. + +Example ${program}.conf file entries: + ## single host update + protocol=domeneshop + login=username + password=your-password + my.example.com + +EoEXAMPLE +} + +###################################################################### +## nic_domeneshop_update +###################################################################### +sub nic_domeneshop_update { + debug("\nnic_domeneshop_update -------------------"); + + my $endpointPath = "/v0/dyndns/update"; + + ## update each configured host + ## should improve to update in one pass + foreach my $h (@_) { + my $ip = delete $config{$h}{'wantip'}; + info("Setting IP address to %s for %s", $ip, $h); + verbose("UPDATE:", "Updating %s", $h); + + # Set the URL that we're going to to update + my $url; + $url = $globals{'ssl'} ? "https://" : "http://"; + $url .= "$config{$h}{'server'}$endpointPath?hostname=$h&myip=$ip"; + + # Try to get URL + my $reply = geturl( + proxy => opt('proxy'), + url => $url, + login => $config{$h}{'login'}, + password => $config{$h}{'password'}, + ); + + # No response, declare as failed + if (!defined($reply) || !$reply) { + failed("Updating %s: Could not connect to %s.", $h, $config{$h}{'server'}); + next; + } + next if !header_ok($h, $reply); + + # evaluate response + my @reply = split /\n/, $reply; + my $status = shift(@reply); + my $message = pop(@reply); + if ($status =~ /204/) { + $config{$h}{'ip'} = $ip; + $config{$h}{'mtime'} = $now; + $config{$h}{'status'} = 'good'; + success("updating %s: good: IP address set to %s", $h, $ip); + } else { + $config{$h}{'status'} = 'failed'; + failed("updating %s: Server said: '%s' '%s'", $h, $status, $message); + } + } +} + + ###################################################################### ## nic_zoneedit1_examples ###################################################################### @@ -4680,10 +4835,12 @@ sub nic_easydns_update { my @hosts = @{$groups{$sig}}; my $hosts = join(',', @hosts); my $h = $hosts[0]; - my $ip = $config{$h}{'wantip'}; - delete $config{$_}{'wantip'} foreach @hosts; + my $ipv4 = $config{$h}{'wantipv4'}; + my $ipv6 = $config{$h}{'wantipv6'}; + delete $config{$_}{'wantipv4'} foreach @hosts; + delete $config{$_}{'wantipv6'} foreach @hosts; - info("setting IP address to %s for %s", $ip, $hosts); + info("setting IP address to %s %s for %s", $ipv4, $ipv6, $hosts); verbose("UPDATE:", "updating %s", $hosts); #'https://api.cp.easydns.com/dyn/generic.php?hostname=test.burry.ca&myip=10.20.30.40&wildcard=ON' @@ -4692,7 +4849,11 @@ sub nic_easydns_update { $url = "https://$config{$h}{'server'}$config{$h}{'script'}?"; $url .= "hostname=$hosts"; $url .= "&myip="; - $url .= $ip if $ip; + $url .= $ipv4 if $ipv4; + foreach my $ipv6a ($ipv6) { + $url .= "&myip="; + $url .= $ipv6a + } $url .= "&wildcard=" . ynu($config{$h}{'wildcard'}, 'ON', 'OFF', 'OFF') if defined $config{$h}{'wildcard'}; if ($config{$h}{'mx'}) { @@ -4729,9 +4890,10 @@ sub nic_easydns_update { $config{$h}{'status'} = $status; if ($status eq 'NOERROR') { - $config{$h}{'ip'} = $ip; + $config{$h}{'ipv4'} = $ipv4; + $config{$h}{'ipv6'} = $ipv6; $config{$h}{'mtime'} = $now; - success("updating %s: %s: IP address set to %s", $h, $status, $ip); + success("updating %s: %s: IP address set to %s %s", $h, $status, $ipv4, $ipv6); } elsif ($status =~ /TOOSOON/) { ## make sure we wait at least a little @@ -5910,6 +6072,82 @@ sub nic_googledomains_update { } } +###################################################################### +## nic_mythicdyn_examples +## +## written by Reuben Thomas +## +###################################################################### +sub nic_mythicdyn_examples { + return <<"EoEXAMPLE"; +o 'mythicdyn' + +The 'mythicdyn' protocol is used by the Dynamic DNS service offered by +www.mythic-beasts.com. + +Configuration variables applicable to the 'mythicdyn' protocol are: + protocol=mythicdyn ## + ipv6=no|yes ## whether to set an A record (default, ipv6=no) + ## or AAAA record (ipv6=yes). + login=service-login ## the user name provided by the admin interface + password=service-password ## the password provided by the admin interface + fully.qualified.host ## the host registered with the service + +Note: this service automatically sets the IP address to that from which the +request comes, so the IP address detected by ddclient is only used to keep +track of when it needs updating. + +Example ${program}.conf file entries: + ## Single host update. + protocol=mythicdyn, \\ + login=service-login \\ + password=service-password, \\ + host.example.com + + ## Multiple host update. + protocol=mythicdyn, \\ + login=service-login \\ + password=service-password, \\ + hosta.example.com,hostb.sub.example.com +EoEXAMPLE +} +###################################################################### +## nic_mythicdyn_update +###################################################################### +sub nic_mythicdyn_update { + debug("\nnic_mythicdyn_update --------------------"); + + # Update each set configured host. + foreach my $h (@_) { + info("%s -- Setting IP address.", $h); + + my $ipversion = $config{$h}{'ipv6'} ? '6' : '4'; + + my $reply = geturl( + proxy => opt('proxy'), + url => "https://ipv$ipversion.$config{$h}{'server'}/dns/v2/dynamic/$h", + method => 'POST', + login => $config{$h}{'login'}, + password => $config{$h}{'password'}, + ipversion => $ipversion, + ); + unless ($reply) { + failed("Updating service %s failed: %s", $h, $config{$h}{'server'}); + next; + } + + my $ok = header_ok($h, $reply); + if ($ok) { + $config{$h}{'mtime'} = $now; + $config{$h}{'status'} = "good"; + + success("%s -- Updated successfully.", $h); + } else { + failed("%s -- Failed to update.", $h); + } + } +} + ###################################################################### ## nic_nsupdate_examples ###################################################################### @@ -6282,7 +6520,7 @@ sub nic_hetzner_update { $config{$domain}{"status-ipv$ipv"} = 'failed'; # Get DNS 'A' or 'AAAA' record ID - $url = "https://$config{$key}{'server'}/records"; + $url = "https://$config{$key}{'server'}/records?zone_id=$zone_id"; $reply = geturl(proxy => opt('proxy'), url => $url, headers => $headers @@ -6299,7 +6537,7 @@ sub nic_hetzner_update { next; } # Pull the ID out of the json, messy - my ($dns_rec_id) = map { ($_->{name} eq $hostname && $_->{type} eq $type && &zone_id eq $_{zone_id}) ? $_->{id} : ()} @{$response->{records}}; + my ($dns_rec_id) = map { ($_->{name} eq $hostname && $_->{type} eq $type) ? $_->{id} : ()} @{$response->{records}}; # Set domain my $http_method=""; @@ -6511,7 +6749,11 @@ sub nic_duckdns_update { $url .= $h; $url .= "&token="; $url .= $config{$h}{'password'}; - $url .= "&ip="; + if (is_ipv6($ip)) { + $url .= "&ipv6="; + } else { + $url .= "&ip="; + } $url .= $ip; @@ -6526,15 +6768,26 @@ sub nic_duckdns_update { next if !header_ok($h, $reply); my @reply = split /\n/, $reply; - my $returned = pop(@reply); - if ($returned =~ /OK/) { - $config{$h}{'ip'} = $ip; - $config{$h}{'mtime'} = $now; - $config{$h}{'status'} = 'good'; - success("updating %s: good: IP address set to %s", $h, $ip); - } else { - $config{$h}{'status'} = 'failed'; - failed("updating %s: Server said: '%s'", $h, $returned); + my $state = 'noresult'; + my $line = ''; + + foreach $line (@reply) { + if ($line eq 'OK') { + $config{$h}{'ip'} = $ip; + $config{$h}{'mtime'} = $now; + $config{$h}{'status'} = 'good'; + $state = 'result'; + success("updating %s: good: IP address set to %s", $h, $ip); + + } elsif ($line eq 'KO') { + $config{$h}{'status'} = 'failed'; + $state = 'result'; + failed("updating %s: Server said: '%s'", $h, $line); + } + } + + if ($state eq 'noresult') { + failed("updating %s: Server said: '%s'", $h, $line); } } } @@ -7005,8 +7258,8 @@ sub nic_ovh_update { } my @reply = split /\n/, $reply; - my $returned = pop(@reply); - if ($returned =~ /good/ || $returned =~ /nochg/) { + my $returned = List::Util::first { $_ =~ /good/ || $_ =~ /nochg/ } @reply; + if ($returned) { $config{$h}{'ip'} = $ip; $config{$h}{'mtime'} = $now; $config{$h}{'status'} = 'good'; @@ -7017,7 +7270,270 @@ sub nic_ovh_update { } } else { $config{$h}{'status'} = 'failed'; - failed("updating %s: Server said: '%s'", $h, $returned); + failed("updating %s: Server said: '%s'", $h, $reply); + } + } +} + +###################################################################### +## nic_porkbun_examples +###################################################################### +sub nic_porkbun_examples { + return <<"EoEXAMPLE"; +o 'porkbun' + +The 'porkbun' protocol is used for porkbun (https://porkbun.com/). +The API is documented here: https://porkbun.com/api/json/v3/documentation + +Before setting up, it is necessary to create your API Key by referring to the following page. + +https://kb.porkbun.com/article/190-getting-started-with-the-porkbun-api + +Available configuration variables: + * apikey (required): API Key of Porkbun API + * secretapikey (required): Secret API Key of Porkbun API + * on-root-domain=yes or no (default: no): Indicates whether the specified domain name (FQDN) is + an unnamed record (Zone APEX) in a zone. + It is useful to specify it as a local variable as shown in the example. + * usev4, usev6 : These configuration variables can be specified as local variables to override + the global settings. It is useful to finely control IPv4 or IPv6 as shown in the example. + * use (deprecated) : This parameter is deprecated but can be overridden like the above parameters. + +Limitations: + * Multiple same name records (for round robin) are not supported. + The same IP address is set for all, creating meaningless extra records. + +Example ${program}.conf file entry: + protocol=porkbun + apikey=APIKey + secretapikey=SecretAPIKey + host.example.com,host2.sub.example.com + on-root-domain=yes example.com,sub.example.com + +Additional example to finely control IPv4 or IPv6 : + # Example 01 : Global enable both IPv4 and IPv6, and update both records. + usev4=webv4 + usev6=ifv6, ifv6=enp1s0 + + protocol=porkbun + apikey=APIKey + secretapikey=SecretAPIKey + host.example.com,host2.sub.example.com + + # Example 02 : Global enable only IPv4, and update only IPv6 record. + usev4=webv4 + + protocol=porkbun + apikey=APIKey + secretapikey=SecretAPIKey + usev6=ifv6, ifv6=enp1s0, usev4=disabled ipv6.example.com + +EoEXAMPLE +} + +###################################################################### +## nic_porkbun_update +###################################################################### +sub nic_porkbun_update { + debug("\nnic_porkbun_update -------------------"); + + ## update each configured host + ## should improve to update in one pass + foreach my $host (@_) { + my ($sub_domain, $domain); + if ($config{$host}{'on-root-domain'}) { + $sub_domain = ''; + $domain = $host; + } else { + ($sub_domain, $domain) = split(/\./, $host, 2); + } + my $ipv4 = delete $config{$host}{'wantipv4'}; + my $ipv6 = delete $config{$host}{'wantipv6'}; + if (is_ipv4($ipv4)) { + info("setting IPv4 address to %s for %s", $ipv4, $host); + verbose("UPDATE:","updating %s", $host); + + my $url = "https://porkbun.com/api/json/v3/dns/retrieveByNameType/$domain/A/$sub_domain"; + my $data = encode_json({ + secretapikey => $config{$host}{'secretapikey'}, + apikey => $config{$host}{'apikey'}, + }); + my $header = "Content-Type: application/json\n"; + my $reply = geturl( + proxy => opt('proxy'), + url => $url, + headers => $header, + method => 'POST', + data => $data, + ); + # No response, declare as failed + if (!defined($reply) || !$reply) { + $config{$host}{'status'} = "bad"; + failed("updating %s: Could not connect to porkbun.com.", $host); + next; + } + if (!header_ok($host, $reply)) { + $config{$host}{'status'} = "bad"; + failed("updating %s: failed (%s)", $host, $reply); + next; + } + # Strip header + # Porkbun sends data in chunks, so it is assumed to be one chunk and parsed forcibly. + $reply =~ qr/{(?:[^{}]*|(?R))*}/mp; + my $response = eval { decode_json(${^MATCH}) }; + if (!defined($response)) { + $config{$host}{'status'} = "bad"; + failed("%s -- Unexpected service response.", $host); + next; + } + if ($response->{status} ne 'SUCCESS') { + $config{$host}{'status'} = "bad"; + failed("%s -- Unexpected status. (status = %s)", $host, $response->{status}); + next; + } + my $records = $response->{records}; + if (ref($records) eq 'ARRAY' && defined $records->[0]->{'id'}) { + my $count = scalar(@{$records}); + if ($count > 1) { + warning("updating %s: There are multiple applicable records. Only first record is used. Overwrite all with the same content."); + } + my $current_content = $records->[0]->{'content'}; + if ($current_content eq $ipv4) { + $config{$host}{'status'} = "good"; + success("updating %s: skipped: IPv4 address was already set to %s.", $host, $ipv4); + next; + } + my $ttl = $records->[0]->{'ttl'}; + my $notes = $records->[0]->{'notes'}; + debug("ttl = %s", $ttl); + debug("notes = %s", $notes); + $url = "https://porkbun.com/api/json/v3/dns/editByNameType/$domain/A/$sub_domain"; + $data = encode_json({ + secretapikey => $config{$host}{'secretapikey'}, + apikey => $config{$host}{'apikey'}, + content => $ipv4, + ttl => $ttl, + notes => $notes, + }); + $reply = geturl( + proxy => opt('proxy'), + url => $url, + headers => $header, + method => 'POST', + data => $data, + ); + # No response, declare as failed + if (!defined($reply) || !$reply) { + failed("updating %s: Could not connect to porkbun.com.", $host); + next; + } + if (!header_ok($host, $reply)) { + failed("updating %s: failed (%s)", $host, $reply); + next; + } + $config{$host}{'status'} = "good"; + success("updating %s: good: IPv4 address set to %s", $host, $ipv4); + next; + } else { + $config{$host}{'status'} = "bad"; + failed("updating %s: No applicable existing records.", $host); + next; + } + } else { + info("No IPv4 address for %s", $host); + } + if (is_ipv6($ipv6)) { + info("setting IPv6 address to %s for %s", $ipv6, $host); + verbose("UPDATE:","updating %s", $host); + + my $url = "https://porkbun.com/api/json/v3/dns/retrieveByNameType/$domain/AAAA/$sub_domain"; + my $data = encode_json({ + secretapikey => $config{$host}{'secretapikey'}, + apikey => $config{$host}{'apikey'}, + }); + my $header = "Content-Type: application/json\n"; + my $reply = geturl( + proxy => opt('proxy'), + url => $url, + headers => $header, + method => 'POST', + data => $data, + ); + # No response, declare as failed + if (!defined($reply) || !$reply) { + $config{$host}{'status'} = "bad"; + failed("updating %s: Could not connect to porkbun.com.", $host); + next; + } + if (!header_ok($host, $reply)) { + $config{$host}{'status'} = "bad"; + failed("updating %s: failed (%s)", $host, $reply); + next; + } + # Strip header + # Porkbun sends data in chunks, so it is assumed to be one chunk and parsed forcibly. + $reply =~ qr/{(?:[^{}]*|(?R))*}/mp; + my $response = eval { decode_json(${^MATCH}) }; + if (!defined($response)) { + $config{$host}{'status'} = "bad"; + failed("%s -- Unexpected service response.", $host); + next; + } + if ($response->{status} ne 'SUCCESS') { + $config{$host}{'status'} = "bad"; + failed("%s -- Unexpected status. (status = %s)", $host, $response->{status}); + next; + } + my $records = $response->{records}; + if (ref($records) eq 'ARRAY' && defined $records->[0]->{'id'}) { + my $count = scalar(@{$records}); + if ($count > 1) { + warning("updating %s: There are multiple applicable records. Only first record is used. Overwrite all with the same content."); + } + my $current_content = $records->[0]->{'content'}; + if ($current_content eq $ipv6) { + $config{$host}{'status'} = "good"; + success("updating %s: skipped: IPv6 address was already set to %s.", $host, $ipv6); + next; + } + my $ttl = $records->[0]->{'ttl'}; + my $notes = $records->[0]->{'notes'}; + debug("ttl = %s", $ttl); + debug("notes = %s", $notes); + $url = "https://porkbun.com/api/json/v3/dns/editByNameType/$domain/AAAA/$sub_domain"; + $data = encode_json({ + secretapikey => $config{$host}{'secretapikey'}, + apikey => $config{$host}{'apikey'}, + content => $ipv6, + ttl => $ttl, + notes => $notes, + }); + $reply = geturl( + proxy => opt('proxy'), + url => $url, + headers => $header, + method => 'POST', + data => $data, + ); + # No response, declare as failed + if (!defined($reply) || !$reply) { + failed("updating %s: Could not connect to porkbun.com.", $host); + next; + } + if (!header_ok($host, $reply)) { + failed("updating %s: failed (%s)", $host, $reply); + next; + } + $config{$host}{'status'} = "good"; + success("updating %s: good: IPv6 address set to %s", $host, $ipv4); + next; + } else { + $config{$host}{'status'} = "bad"; + failed("updating %s: No applicable existing records.", $host); + next; + } + } else { + info("No IPv6 address for %s", $host); } } } @@ -7389,6 +7905,90 @@ sub nic_regfishde_update { } } +###################################################################### +###################################################################### +## enom +###################################################################### +sub nic_enom_examples { + return < opt('proxy'), + url => $url + ); + + if (!defined($reply) || !$reply) { + failed("updating %s: Could not connect to %s.", $h, $config{$h}{'server'}); + last; + } + + last if !header_ok($h, $reply); + + my @reply = split /\n/, $reply; + + if (grep /Done=true/i, @reply) { + $config{$h}{'ip'} = $ip; + $config{$h}{'mtime'} = $now; + $config{$h}{'status'} = 'good'; + success("updating %s: good: IP address set to %s", $h, $ip); + } else { + $config{$h}{'status'} = 'failed'; + warning("SENT: %s", $url) unless opt('verbose'); + warning("REPLIED: %s", $reply); + failed("updating %s: Invalid reply.", $h); + } + } +} + # Execute main() if this file is run as a script or run via PAR (https://metacpan.org/pod/PAR), # otherwise do nothing. This "modulino" pattern makes it possible to import this file as a module # and test its functions directly; there's no need for test-only command-line arguments or stdout diff --git a/sample-etc_rc.d_ddclient.freebsd b/sample-etc_rc.d_ddclient.freebsd deleted file mode 100755 index d8dc341..0000000 --- a/sample-etc_rc.d_ddclient.freebsd +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/sh - -# PROVIDE: ddclient -# REQUIRE: LOGIN -# KEYWORD: shutdown -# -# Add the following lines to /etc/rc.conf.local or /etc/rc.conf -# to enable this service: -# -# ddclient_enable (bool): Set to NO by default. -# Set it to YES to enable ddclient. - -. /etc/rc.subr - -name=ddclient -rcvar=ddclient_enable -ddclient_conf="/etc/ddclient/ddclient.conf" - -command="/usr/local/sbin/${name}" -load_rc_config $name - -delay=$(grep -v '^\s*#' "${ddclient_conf}" | grep -i -m 1 "daemon" | awk -F '=' '{print $2}') - -if [ -z "${delay}" ] -then - ddclient_flags="-daemon 300" -else - ddclient_flags="" -fi - -run_rc_command "$1" diff --git a/sample-etc_rc.d_init.d_ddclient b/sample-etc_rc.d_init.d_ddclient deleted file mode 100755 index 5eb9b40..0000000 --- a/sample-etc_rc.d_init.d_ddclient +++ /dev/null @@ -1,100 +0,0 @@ -#!/bin/bash -# -# ddclient This shell script takes care of starting and stopping -# ddclient. -# -# chkconfig: 2345 65 35 -# description: ddclient provides support for updating dynamic DNS services. - -CONF=/etc/ddclient/ddclient.conf -program=ddclient - -[ -f $CONF ] || exit 0 - -system=unknown -if [ -f /etc/fedora-release ]; then - system=fedora -elif [ -f /etc/redhat-release ]; then - system=redhat -elif [ -f /etc/debian_version ]; then - system=debian -fi - -PID='' -if [ "$system" = "fedora" ] || [ "$system" = "redhat" ]; then - . /etc/init.d/functions - PID=`pidofproc $program` -else - PID=`ps -aef | grep "$program - sleep" | grep -v grep | awk '{print $2}'` -fi - -PATH=/usr/bin:/usr/local/bin:${PATH} -export PATH - -# See how we were called. -case "$1" in - start) - # See if daemon=value is specified in the config file. - # Assumptions: - # * there are no quoted "#" characters before "daemon=" - # (if there is a "#" it starts a comment) - # * "daemon=" does not appear in a password or value - # * if the interval value is 0, it is not quoted - INTERVAL=$(sed -e ' - s/^\([^#]*[,[:space:]]\)\{0,1\}daemon=\([^,[:space:]]*\).*$/\2/ - t quit - d - :quit - q - ' "$CONF") - if [ -z "$DELAY" ] || [ "$DELAY" = "0" ]; then - DELAY="-daemon 300" - else - # use the interval specified in the config file - DELAY='' - fi - echo -n "Starting ddclient: " - if [ "$system" = "fedora" ] || [ "$system" = "redhat" ]; then - daemon $program $DELAY - else - ddclient $DELAY - fi - echo - ;; - stop) - # Stop daemon. - echo -n "Shutting down ddclient: " - if [ -n "$PID" ]; then - if [ "$system" = "fedora" ] || [ "$system" = "redhat" ]; then - killproc $program - else - kill $PID - fi - else - echo "ddclient is not running" - fi - echo - ;; - restart) - $0 stop - $0 start - ;; - status) - if [ "$system" = "fedora" ] || [ "$system" = "redhat" ]; then - status $program - else - if test "$PID"; then - for p in $PID; do - echo "$program (pid $p) is running" - done - else - echo "$program is stopped" - fi - fi - ;; - *) - echo "Usage: ddclient {start|stop|restart|status}" - exit 1 -esac - -exit 0 diff --git a/sample-etc_rc.d_init.d_ddclient.alpine b/sample-etc_rc.d_init.d_ddclient.alpine deleted file mode 100755 index d20d119..0000000 --- a/sample-etc_rc.d_init.d_ddclient.alpine +++ /dev/null @@ -1,38 +0,0 @@ -#!/sbin/openrc-run -description="ddclient Daemon for Alpine" -command="/usr/bin/ddclient" -config_file="/etc/ddclient/ddclient.conf" -command_args="" -pidfile=$(grep -v '^\s*#' "${config_file}" | grep -i -m 1 pid= | awk -F '=' '{print $2}') -delay=$(grep -v '^\s*#' "${config_file}" | grep -i -m 1 "daemon" | awk -F '=' '{print $2}') - -if [ -z "${delay}" ] -then - command_args="-daemon 300" -else - command_args="" -fi - - -depend() { - use logger - need net - after firewall -} - -start() { - ebegin "Starting ddclient" - start-stop-daemon --start \ - --exec "${command}" \ - --pidfile "${pidfile}" \ - -- \ - ${command_args} - eend $? -} - -stop() { - ebegin "Stopping ddclient" - start-stop-daemon --stop --exec "${command}" \ - --pidfile "${pidfile}" - eend $? -} diff --git a/sample-etc_rc.d_init.d_ddclient.lsb b/sample-etc_rc.d_init.d_ddclient.lsb deleted file mode 100755 index bced239..0000000 --- a/sample-etc_rc.d_init.d_ddclient.lsb +++ /dev/null @@ -1,64 +0,0 @@ -#!/bin/sh -# -# ddclient This shell script takes care of starting and stopping -# ddclient. -# -# chkconfig: 2345 65 35 -# description: ddclient provides support for updating dynamic DNS services. -# -# Above is for RedHat and now the LSB part -### BEGIN INIT INFO -# Provides: ddclient -# Required-Start: $syslog $remote_fs -# Should-Start: $time ypbind sendmail -# Required-Stop: $syslog $remote_fs -# Should-Stop: $time ypbind sendmail -# Default-Start: 3 5 -# Default-Stop: 0 1 2 6 -# Short-Description: ddclient provides support for updating dynamic DNS services -# Description: ddclient is a Perl client used to update dynamic DNS -# entries for accounts on many dynamic DNS services and -# can be used on many types of firewalls -### END INIT INFO -# -### - -[ -f /etc/ddclient/ddclient.conf ] || exit 0 - -DDCLIENT_BIN=/usr/bin/ddclient - -# -# LSB Standard (SuSE,RedHat,...) -# -if [ -f /lib/lsb/init-functions ] ; then - . /lib/lsb/init-functions -fi - -# See how we were called. -case "$1" in - start) - echo -n "Starting ddclient " - start_daemon $DDCLIENT_BIN -daemon 300 - rc_status -v - ;; - stop) - echo -n "Shutting down ddclient " - killproc -TERM `basename $DDCLIENT_BIN` - rc_status -v - ;; - restart) - $0 stop - $0 start - rc_status - ;; - status) - echo -n "Checking for service ddclient " - checkproc `basename $DDCLIENT_BIN`w - rc_status -v - ;; - *) - echo "Usage: ddclient {start|stop|restart|status}" - exit 1 -esac - -exit 0 diff --git a/sample-etc_rc.d_init.d_ddclient.redhat b/sample-etc_rc.d_init.d_ddclient.redhat deleted file mode 100755 index 2e0fd32..0000000 --- a/sample-etc_rc.d_init.d_ddclient.redhat +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/sh -# -# ddclient This shell script takes care of starting and stopping -# ddclient. -# -# chkconfig: 2345 65 35 -# description: ddclient provides support for updating dynamic DNS services. - -[ -f /etc/ddclient/ddclient.conf ] || exit 0 - -. /etc/rc.d/init.d/functions - -# See how we were called. -case "$1" in - start) - # Start daemon. - echo -n "Starting ddclient: " - touch /var/lock/subsys/ddclient - daemon ddclient -daemon 300 - echo - ;; - stop) - # Stop daemon. - echo -n "Shutting down ddclient: " - killproc ddclient - echo - rm -f /var/lock/subsys/ddclient - ;; - restart) - $0 stop - $0 start - ;; - status) - status ddclient - ;; - *) - echo "Usage: ddclient {start|stop|restart|status}" - exit 1 -esac - -exit 0 diff --git a/sample-etc_rc.d_init.d_ddclient.ubuntu b/sample-etc_rc.d_init.d_ddclient.ubuntu deleted file mode 100755 index 73451c0..0000000 --- a/sample-etc_rc.d_init.d_ddclient.ubuntu +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/sh -### BEGIN INIT INFO -# Provides: ddclient -# Required-Start: $remote_fs $syslog $network -# Required-Stop: $remote_fs $syslog $network -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: Start ddclient daemon at boot time -# Description: Start ddclient that provides support for updating dynamic DNS services. Originally submitted by paolo martinelli, updated by joe passavanti -### END INIT INFO - -DDCLIENT=/usr/bin/ddclient -CONF=/etc/ddclient/ddclient.conf -PIDFILE=/var/run/ddclient.pid - -test -x $DDCLIENT || exit 0 -test -f $CONF || exit 0 - -. /lib/lsb/init-functions - -case "$1" in - start) - if [ ! -f $PIDFILE ]; then - log_begin_msg "Starting ddclient..." - DELAY=`grep -v '^\s*#' $CONF | grep -i -m 1 "daemon" | awk -F '=' '{print $2}'` - if [ -z "$DELAY" ] ; then - DELAY="-daemon 300" - else - DELAY='' - fi - start-stop-daemon -S -q -p $PIDFILE -x $DDCLIENT -- $DELAY - log_end_msg $? - else - log_warning_msg "Service ddclient already running..." - fi - ;; - stop) - if [ -f $PIDFILE ] ; then - log_begin_msg "Stopping ddclient..." - start-stop-daemon -K -q -p $PIDFILE - log_end_msg $? - rm -f $PIDFILE - else - log_warning_msg "No ddclient running..." - fi - ;; - restart|reload|force-reload) - $0 stop - $0 start - ;; - *) - log_success_msg "Usage: $0 {start|stop|restart|reload|force-reload}" - exit 1 - ;; -esac - -exit 0