README.md 29 KB

dyndns.pl

Perl CGI-BIN script to handle Dynamic DNS updates through HTTP (e.g. from a router), updating DNS records through secure DNS update statements to run your own Dynamic DNS Service.

Version 1.1, latest version, documentation and bugtracker available on my GitLab instance

Copyright (c) 2013 - 2019 Frederik Lindenaar. free for distribution under the GNU License, see below

Introduction

dyndns.pl provides a simple interface to allow Dynamic DNS updates for DNS zones through HTTP requests. It is intended for routers and (aDSL) modems to register their IP address by simply opening a URL (this is supported by most modern devices) but can also be used by end-users (either directly by using a client). The script itself uses DNS' nsupdate calls to perform the update. With this script you can integrate devices not supporting nsupdate and environments where the master DNS server is not publicly available. The script suits my setup/and needs and still might have glitches, but turned out to be a very stable solution the last 6 years on both Linux as well as MacOS.

Please see below on how to setup the client side including:

In case you have any comments / questions or issues, please raise them through my GitLab instance so that other users can benefit and respond. Please also use this to submit setup instructions for other devices you have set up for inclusion in this document.

Setup of the server side

This script is to be executed as CGI-BIN script by a web server. As it is written in Perl, it requires that installed (which is pretty standard on *nix platforms). This description covers the installation on Apache 2.4 and should be similar for other web servers, with ISC Bind v9. For performance reasons consider using the Apache mod_perl module for highly a volatile domain.

Installation

The setup of this solution consists of the following steps:

  1. Ensure that the Perl modules CGI and Net::DNS are installed.

    • on Debian/Ubuntu linux this can be done by:
       sudo apt-get install libcgi-pm-perl libnet-dns-perl
 * or directly from CPAN (assuming that is installed):

   ~~~
   cpan CGI Net::DNS
   ~~~
  1. Install the file dyndns.pl either in your cgi-bin directory or in a separate folder

  2. Update the configuration section at the top of the script to match your environment (see the section on configuration below). The least you need to change $DNSServer to point to your DNS server and you probably want to have a look at the $AllowDebugKey (useful for getting things started but you want to set this to 'off' in production.

  3. To have a nicer URL (or in case the script is not installed in the web server's cgi-bin directory) add the following line to your Apache virtual host configuration (replacing [INSTALL_DIR] with the install directory):

     ScriptAlias /dyndns   [INSTALL_DIR]/dyndns.pl

    in case you have installed the script in a non-standard folder, you will also need the following to make this work on Apache 2.4 (again replacing [INSTALL_DIR] with the install directory):

     <Directory [INSTALL_DIR]/>
           AllowOverride None
           Options +ExecCGI -MultiViews -Indexes
           Require all granted
     </Directory>

    reload apache with /etc/init.d/apache reload to make the script available at http://myserver.mydomain.tld/dyndns.

    It is also possible to run as a virtual host, see below for an example of that.

  4. To setup your Bind nameserver, either update named.conf direcly or create a separate file (e.g. named.dyndns.conf in the Bind configuration directory and include that in your setup with the include directive (e.g. include "named.dyndns.conf";). For a basic dynamic DNS setup a configuration like below is required:

     // Define the keys for DynDNS
     key "dyndns.mydomain.tld" {
         algorithm hmac-md5; secret "QdDJC7QVYmsCxgWoSAUmBg==";
     };
    
     key "siteuser" {
         algorithm hmac-md5; secret "R6Xkbn+FP85Hq3EDNmv+GQ==";
     };
    
     // Define the DDNS zone
     zone "dyndns.mydomain.tld" IN {
          type master;
          file "dyndns/db.dyndns.mydomain.tld";
    
          // enable this for list and expire support
          // allow-transfer { 192.168.0.2; };
    
          update-policy {
                 grant dyndns.mydomain.tld zonesub ANY;
                 grant siteuser name site.dyndns.mydomain.tld ANY;
          };
     };

    The above defines a domain zone file dyndns/db.dyndns.mydomain.tld with two signer/keys. siteuser only can update site.dyndns.mydomain.tld while dyndns.mydomain.tld can update all entries in the domain (intended for expiry). If you intend to use expiry or want to be able to retrieve a list of all entries, uncomment the allow-transfer statement and update the IP adres to that of your web server.

    To seed these entries with fresh keys), use the following commands and copy the generated keys into the config file.

   * to generate a new key *dyndns.mydomain.tld*:

     ~~~
     ddns-confgen -a hmac-md5 -k dyndns.mydomain.tld -z dyndns.mydomain.tld
     ~~~

   * generate the required configuration for *siteuser* (or any new user):

     ~~~
     ddns-confgen -a hmac-md5 -k siteuser -s site.dyndns.mydomain.tld
     ~~~
  1. Generate an initial zone file like the one below for the dyndns domain in the location specified in the config file above.

     $TTL 3600       ; 1 hour
     @               IN  SOA auth.dns.mydomain.tld. hostmaster.mydomain.tld. (
                         2019000001 ; serial
                         43200      ; refresh (12 hours)
                          3600      ; retry (1 hour)
                         86400      ; expire (24 hours)
                           900      ; minimum (15 minutes)
                     )
                     TXT   "Dynamic DNS zone for mydomain.tld"
    
     site            A       1.2.3.4

    Please note that Bind will rewrite this file and you need to be careful with it. Entries do not need to exist initially, as long as the signer/key has access to a hostname, the entry can be created (so the only thing required to setup a new host is to register a signer/key).

    If you do need to update the zone file to change entries, consider using the bind nsupdate command instead. If that is inconvenient, the following steps must be followed not to get our of sync with Bind's zone database (please note that when you have views this works slightly differently):

   * execute the command `rndc freeze [zone]`
   * edit the zone file for [zone]
   * execute the command `rndc unfreeze [zone]`
  1. Last step is to instruct bind to reload it's configuration (rndc reload) and test the setup. please see below how to invoke the script.

    URLs / checks to perform are:

 * <http://myserver.mydomain.tld/dyndns/list?domain=dyndns.mydomain.tld>
   to list the entries in the domain (requires zone transfer rights!)
 * <http://myserver.mydomain.tld/dyndns/update?host=site.dyndns.mydomain.tld&user=siteuser&secret=......>
   to add/update a site and
 * <http://myserver.mydomain.tld/dyndns/delete?host=site.dyndns.mydomain.tld&user=siteuser&secret=......>
   to delete (clear) it.

Please read the section below as well on the configuration and different modes (operations) available.

Configuration

At the top of the script is a "Configuration" section, which contains the configurable options of the scripts. As of version 1.1 the script also supports a configuration file so that modifying the script is no longer required.

Parameter Description
$ConfigFile Enable/disable config file support, see below
$DNSServer IP address of the DNS Server to send DNS update requests to
@DNSDomain How to determine the host's domain name, see below
$DomainListKey Secret required to use the list mode, set to '' to always enable and to 'off' to disable this mode
$ExpandCNAMEs Max. CNAME lookups for $host (0 to disable), see below
$AllowDebugKey Output debug log after result when debug parameter equals this value. Set to '' to always enable and to 'off' to disable debugging
$AuthMode Defines how to authenticate DNS update requests, see below
$StaticSigner Static signer ID to be used for AuthMode static or both
$StaticKey Static signing key to be used for AuthMode static or both
$RequireRR Require an existing DNS record of this type to allow updates
$ExpireAfter Expire time for registrations in minutes, hours, weeks or seconds. Format is number optionally followed by m, h, w, s (seconds is default)
@ReplaceRR List of DNS Record types to remove (clear) as part of update.
$UpdateTXT Add host TXT record during update with this text followed by a timestamp. Used for expiry (so don't change!), leave empty to not add this
$DeleteTXT Set TXT record upon deletion with this text and a timestamp.
$RecordTTL TTL for created records in minutes, hours, weeks or seconds. Format is number optionally followed by m, h, w, s (seconds is default)

Please note: when changing the script all values must be correctly quoted, etc. not to break the script. Therefore as of version 1.1 a config file is supported (preferred), see below.

Configuration File

The script can read its settings from a config located in the same directory as the script and with the extension .cfg (ignoring a .pl extension) so the default config file would be dyndns.cfg. The behavior of how to support the config file is configured through the variable $ConfigFile and can be one of:

  • optional - config file is read if it exists, this is the default
  • required - config file is read and must exist (or the script will fail)
  • ignore - config file is ignored and not read, configuration in the script

The general format of the config file is keyword = value, see the table below for a mapping of the parameters to keywords. For lists (variables starting with a @) the value is comma-separated. The config file supports comments, ignores empty lines, starting/trailing spaces and everything following a #. Refer to dyndns.cfg.dist for an example config file. Please note that the script will fail if it encounters an error or unknown keyword in the config file.

Parameter Config Setting Default value
$AllowDebugKey allow_debug_key off (debugging disabled)
$AuthMode auth_mode remote (see below)
$DeleteTXT delete_txt DynDNS cleared on
$DNSServer dns_server 192.168.1.1
@DNSDomain dns_domain ?, !, 0 (see below)
$DomainListKey domain_list_key off (domain list disabled)
$ExpandCNAMEs expand_cnames 1 (1 level, see below)
$ExpireAfter expire_after 1w (1 week, see below)
$RecordTTL record_ttl 1h (1 hour)
$RequireRR require_rr
@ReplaceRR replace_rr A, AAAA, TXT
$StaticKey static_key
$StaticSigner static_signer
$UpdateTXT update_txt Last DynDNS update on

Please note that since $ConfigFile determines config file support, it cannot be configured in the file. By default the config file is optional not to break existing configurations.

DNS Zone (Domain Name) Selection

In order to send the right update request to the DNS server, the correct DNS zone to update must be determined based on the request's hostname. Most of the time an update for hostname.subdomain.mydomain.tld is an update of hostname the DNS zone subdomain.mydomain.tld and then the defaults are sufficient. However, in some scenarios (e.g. one of my use cases) an update should be sent for hostname hostname.subdomain in the zone mydomain.tld instead. The DNS server cannot figure this out itself (at least ISC's Bind9 can not) so it is implemented here.

The array @DNSDomain contains a list of values matched against the hostname to determine the DNS zone to update and can contain:

Value match hostname ending with
"?" the domain name from parameter domain
"!" server name the HTTP(S) request was sent to
0 domain from hostname (strip of everythin till first .)
positive number last # parts from hostname
negative number last # parts of server name the HTTP(S) request was sent to
any other string use value specified

The first parameter matching the hostname's end will be used. The default is ( '?', '!', 0 ), which should be OK in most cases.

CNAME Support

The script supports using separate subdomain (e.g. dyndns.mydomain.tld) for dynamic DNS and CNAMEs to entries in that subdomain from another zone (e.g. mydomain.tld). The advantage of such a setup is that only one zone (SOA file) within the domain will have frequent updates (and hence requires a short TTL so prevent it from being cached) while the rest of the domain's zones can be cached.

The user does not have to notice this at all as script supports check whether the host provided is a CNAME and if so, performs the request for the actual hostname instead of the provided one. The value of $ExpandCNAMEs determines the maximum number of CNAME lookups supported (so nesting is allowed and this limits the level of nesting to prevent loops).

To disable lookups for CNAME expansion, set $ExpandCNAMEs to 0.

Authentication Modes

For signing DNS update requests sent to the DNS server the script supports 3 ways to obtain the signer and key:

AuthMode Description
static use only static authentication information from $StaticSigner and$StaticKey (and ignore authentication information provided in the request)
remote use only authentication information provided in the request
both use authentication information provided in the request (fields user and secret) when provided, otherwise use static values from $StaticSigner and $StaticKey. Please note that this is checked per parameter

Supported Operations

The script can perform the following operations (modes):

Mode Description Required Parameters Optional Parameters
list List zone *** secret *** domain**
view Show host's DNS entry host
update Update/add a DDNS host host + auth.* ipv4addr, ipv6addr
delete Remove registration host + auth.*
expire Expire registrations domain** + auth.*

* modes that change DNS require authentication, depending on the value of $AuthMode the parameters user and secret may be required ($AuthMode remote) required or optional ($AuthMode both)

** in case domain is omitted, it will be determined using the host parameter, if provided, or by using the virtualhost the script runs on based on the @DNSDomain setting

*** list mode is only available when $DomainListKey is not set to off, in case $DomainListKey is not empty, secret is required and must equal the key in $DomainListKey

Request Parameters

The script supports (requires) the following parameters (please see the table above for which is needed for what mode):

Parameter Description
mode the action to perform (if not provided as part of the path name)
domain domain for list/expire request, determined from host if ommitted
host hostname to act on, expand CNAMEs max. $ExpandCNAMEs levels deep
ip alias / shortcut for ipv4addr
ipv4addr The IPv4 address to register for the host (update mode only) *
ipv6 alias / shortcut for ipv6addr
ipv6addr The IPv6 address to register for the host (update mode only) *
user signer of the DNS Update, used for AuthMode remote and both
secret key to sign the DNS Update, used for AuthMode remote and both, also used as $DomainListKey for list mode.
debug debug key, show debug information if this equals $AllowDebugKey

* in update mode, if ipv4addr or ipv6addr is set to auto in the request, the CGI variable $REMOTE_ADDR (the client address), its value will be used instead as IPv4/IPv6 address. Please Note that if both are omitted existing addresses will be removed!

Invoking the script

The script is implemented using the perl CGI module so for testing purposes it can be called from the command line with parameters as arguments, i.e.

./dyndns.pl mode=expire domain=mydomain.tld debug=....

Which is quite handy for debugging. Please note that the Perl CGI library sets $REMOTE_ADDR to 127.0.0.1, the server name in this case will be localhost and that the output is the HTML result.

The standard way to use the script is to place it in the cgi-bin folder your server, which allows it to be called as:

http://myserver.mydomain.tld/cgi-bin/dyndns.pl?mode=list&domain=mydomain.tld&debug=...

As per the setup instruction above, there are various ways to make the URL cleaner, i.e.

http://myserver.mydomain.tld/dyndns?mode=list&domain=mydomain.tld&debug=...

The script also supports include the mode variable as part of the location (using and the CGI variable $PATH_INFO to set the mode), i.e.

http://myserver.mydomain.tld/cgi-bin/dyndns.pl/list?domain=mydomain.tld&debug=...

When combining the setup would become:

http://myserver.mydomain.tld/dyndns/list?domain=mydomain.tld&debug=...

If using a dedicated virtual host see below it becomes:

http://myserver.mydomain.tld/list?domain=mydomain.tld&debug=...

Which is how I use it.

Expiring Records

The script can expire registrations after a while. For this, it must add a TXT record containing the date of the last change (on by default) and when requested it will remove any entry older than the value configured in $ExpireAfter.

Please note that:

  • as this is dependent on the value of a TXT record, it may fail if these records are updated through another method.
  • there is no security implemented (other than the value of $ExpireAfter)

To initiate the expiry, the script must be called with two parameters:

  1. mode should be set to expire
  2. domain must be set to the DNS Zone (domain) to run against.

Both can be setup easily in cron with entries like:

# Samples to run the expiry every hour

# Cron fields definition:
#.---------------- minute (0 - 59)
#| .------------- hour (0 - 23)
#| |  .---------- day of month (1 - 31)
#| |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
#| |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
#| |  |  |  |
#* *  *  *  * user-name command to be executed

# Directly run the script, does not require specific permissions
15 *  *  *  * www-data  [INSTALL_DIR]/dyndns.pl mode=expire domain=mydomain.tld

# example using curl
15 *  *  *  * www-data  curl https://myserver.mydomain.tld/expire?domain=mydomain.tld > /dev/null

Name Server Setup Requirements

As the script is only translating requests, depends heavily on the setup of the nameserver. The DNS server (obviously) needs to allow DNS updates. In addition to the setup described above, please note that:

  • For the modes list and expire to work, the script needs to perform a DNS zone transfer (AXFR). This must be allowed for the host running the script.
  • for each DDNS host, a signer and key must have the rights to change the entry (one signer/key can be setup to change multiple hosts).
  • The expire mode requires a signer and key that can change all DDNS hosts within the domain.
  • The script currently only supports HMAC-MD5 type keys (limitation of the used Perl Net::DNS library). The keys setup in the nameservers must therefore be of the same time or authentication won't work.

This setup has been tested against ISC Bind version 9 and scales pretty well. Adding the keys to the nameserver configuration is still manual in my setup but bit difficult script, if needed.

Configure as Virtual Host

Running dyndns.pl on a Virtual Host is possible using mod_rewrite. This is how I use it as it allows the URLs to become even more simple, e.g.:

  • to update: https://dyndns.mydomain.tld/update/hostname.mydomain.tld?secret=...
  • to delete: https://dyndns.mydomain.tld/delete/hostname.mydomain.tld?secret=...
  • to view: https://dyndns.mydomain.tld/view/hostname.mydomain.tld
  • to list: https://dyndns.mydomain.tld/list/mydomain.tld?secret=...
  • to expire: https://dyndns.mydomain.tld/expire/mydomain.tld

An example Apache 2.4 config is shown below (please replace [INSTALL_DIR] with the install directory and obviously replace the server name as well):

<VirtualHost *:80 *:443>
  ServerName dyndns.mydomain.tld

  # Enable URL Rewriting
  RewriteEngine On

  # Enforce HTTPS access
  RewriteCond %{HTTPS} off
  RewriteRule /         https://%{HTTP_HOST}%{REQUEST_URI}      [R]

  # re-route everything to the dyndns script
  RewriteRule (.*)      /dyndns/$1                              [PT]

  ScriptAlias /dyndns           [INSTALL_DIR]/dyndns.pl

  <Directory [INSTALL_DIR]>
        AllowOverride None
        Options +ExecCGI -MultiViews
        Require all granted
  </Directory>

</VirtualHost>

Integration with devices

Integration on routers and other devices is straigtforward, provided do support DDNS registrations using a custom URL. The Basic format for the registration URL to register is:

https://SERVER/cgi-bin/dyndns/update?host=HOSTNAME&ip=IPADDRESS&secret=KEY

Check the list of parameters supported for all available options, the above URL contains the absolute minimum where:

Parameter Value
SERVER is the host the script is installed on
HOSTNAME is the client's hostname as configured in the DNS server
SECRET is the secret key as configured in the DNS server
IPADDRESS is the ipv4 address (often dynamic, can also be set to auto)

Depending on how you have configured the URL of the script to be, the path (/cgi-bin/dyndns/ may need to be altered as per your setup).

Please note that:

  • The generated secret may contain a +, which must be encoded correctly in the request or it will fail. I found that not all clients (e.g. a Fritz!Box) do this correctly, make sure that your secrets either don't contain a + or encode it manually (replace any + with %2B in that case).
  • In case the IP address of the device is behind NAT and you want to have the public address register, use the auto value for parameters ip/ipv4addr and ipv6/ipv6addr to have the script auto-detect it (though that this can only be used for either an IPv4 or an IPv6 address and will only work for devices registering using that protocol!)
  • Some devices have a preference to connect over IPv6 (e.g. Cisco routers). This can be used to register the IPv4 and IPv6 addresses together by passing the IPv4 address as parameter en setting the IPv6 parameter to auto.
  • Some devices (e.g. a Fritz!Box) support a separate URL for IPv4 and IPv6 registrations. Unfortunately this script cannot handle this yet and will unregister a previous registration when the second request comes in. Please raise a ticket if you have such a situation to work on a solution together.

To check whether the client's registration was successful (and correct) visit:

https://SERVER/cgi-bin/dyndns/view?host

Cisco Routers

For Cisco routers add the following config:

ip ddns update method DYNDNS
 HTTP
  add https://SERVER/cgi-bin/dyndns/update?host=<h>&ip=<a>&secret=SECRET
  remove https://SERVER/cgi-bin/dyndns/delete?host=<h>&secret=SECRET
 interval maximum 0 1 0 0

replacing SERVER for the host the script is installed on and SECRET for a DNS key authorized to update the record. The cisco router will replace and with the IPv4 address and hostname.

To setup interface Dialer0 to register as hostname.dyndns.mydomain.tld add:

interface Dialer0
 ip ddns update hostname hostname.dyndns.mydomain.tld
 ip ddns update DYNDNS

Which instructs to register using the address of Dialer0 as soon as that is up or changes (this also works for non-dialer devices).

Please note that before entering the ? as part of the URL, a CTRL-V is required to prevent the Cisco CLI to list the available command parameters.

AVM Fritz!Box routers

To setup DynDNS on a Fritz!Box perform the following steps:

  • Login to your Fritz!Box as an admin user
  • Open the 'Internet' menu an go through the 'External Access' page
  • Open the 'DynDNS' tab
  • Enable the 'Use DynDNS' checkbox
  • Select DynDNS Provider: 'User-defined'
  • Enter the following data (replacing YOURDOMAIN with your DynDNS domain and SERVER with your server name - check the rest of the URL as well!)
    • Update URL: https://SERVER/cgi-bin/dyndns/update?host=<domain>&ip=<ipaddr>&secret=<pass>
    • Domain name: hostname setup in DNS
    • Username/Email: put here something, not used unless you add it to the URL
    • Password: secret key setup in DNS
  • Click 'Apply' to store and activate the DDNS registrations

Check this page for the available parameters that can be substituted in the URL.

The status of the DynDNS registrations can be seen in the 'Internet' menu on the 'Online Monitor' page.

To stop DynDNS registrations, uncheck 'Use DynDNS' from the same screen.

Synology DSM (NAS)

To setup DynDNS on a Synology NAS (DSM 6 or later) perform the following steps:

  • Login to your Synology NAS DSM as an admin user
  • Open the Control Panel and go to 'External Access'
  • Click 'Customize' to add a new DDNS provider
  • Enter the following data (replacing YOURDOMAIN with your DynDNS domain and SERVER with your server name - check the rest of the URL as well!)
    • Service Provider: YOURDOMAIN
    • Query URL https://SERVER/cgi-bin/dyndns/update?host=__HOSTNAME__&ip=__MYIP__&secret=__PASSWORD__
  • Click Save to store the custom DDNS provider
  • Click Add to register the DDNS registration and enter:
    • Service Provider: select the name you have just added (*YOURDOMAIN)
    • Hostname: hostname setup in DNS
    • Username/Email: put here something, not used unless you add it to the URL
    • Password/Key: secret key setup in DNS
  • Click 'OK' to store and activate the DDNS registrations

After a while the screen should display that the status is Normal and when the last update occurred.

To stop DDNS registrations, 'Delete' the registration from the same screen.

License

This script, documentation and configuration examples are free software: you can redistribute and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This script, documentation and configuration examples are distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, download it from http://www.gnu.org/licenses/.