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.

**Version 1.0**, latest version, documentation and bugtracker available on my
[GitLab instance](https://gitlab.lindenaar.net/scripts/dyndns)

Copyright (c) 2013 - 2015 Frederik Lindenaar. free for distribution under the
GNU License, see [below](#license)


Introduction
------------
This script provides a simple interface to allow Dynamic DNS updates for DNS
zones. It is intended to be used for routers and (aDSL) modems to register their
IP address by simply opening a URL (this is supported by many modern devices)
but can also be used by end-users (either directly by using a client). Please
bear in mind that this script suits my setup and still might have glitches, but
so far turned out to be a quite stable solution for my needs and I use it in a
production setup. In case you have any comments / questions or issues, please
raise them through my
[GitLab instance](https://gitlab.lindenaar.net/scripts/dyndns) so that all
users benefit.

Setup
-----
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 nowadays
on all *nix platforms). This description covers the installation on Apache 2.4,
which 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.

The setup of this solution consists of the following steps:

  1. Ensure that the Perl modules CGI and Net::DNS are installed.
     * on Debian/Ubunto linux this can be done by:

       ~~~~
       sudo apt-get install libcgi-pm-perl libnet-dns-perl
       ~~~~

     * or if you have cpan installed:

       ~~~
       cpan CGI Net::DNS
       ~~~

  2. Install the file `dyndns.pl` either in your cgi-bin directory or in a
     separate folder

  3. Update the configuration section at the top of the script to match your
     environment (see the section on [configuration](#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.

  4. 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>

  5. 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, comment out 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
         ~~~

  6. 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. (
                         2015051401 ; 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]`

  7. 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](#invoking).

     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&key=......>
       to add/update a site and
     * <http://myserver.mydomain.tld/dyndns/delete?host=site.dyndns.mydomain.tld&user=siteuser&key=......>
       to delete (clear) it.

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

<a name=configuration>Configuration</a>
---------------------------------------

At the top of the script is a "Configuration" section, which contains the
configurable options of the scripts.

Parameter        | Description
:----------------+:-------------------------------------------------------------
`$DNSServer`     | IP address of the DNS Server to send DNS update requests to
`$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. 

Please note that the values must be correctly quoted, etc. not to break the script.


#### 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   | Show DDNS domain entries | `domain`__**__      |
view   | Show DDNS hostname entry | `host`              |
update | Update/add a DDNS host   | `host` + auth.__*__ | `ipv4addr`, `ipv6addr`
delete | Remove DDNS registration | `host` + auth.__*__ |
expire | Expire registrations     | `domain`__**__ + auth.__*__

__*__   Modes that change registrations 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


#### Parameters
The script supports 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) __*__
`ipv6addr`| The IPv6 address to register for the host (update mode only) __*__
`user`    | signer of the DNS Update, used for `AuthMode` *remote* and *both*
`key`     | key to sign the DNS Update, used for `AuthMode` *remote* and *both*
`debug`   | debug key, show debug information if this equals `$AllowDebugKey`

__*__   in update mode, if `ipv4addr` or `ipv6addr` is not provided with the
        request, the CGI variable `$REMOTE_ADDR` (the client address), its value
        will be used instead as IPv4/IPv6 address.


#### <a name="invoking">Invoking the script</a>
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 purposes. Please note that the Perl CGI
library sets `$REMOTE_ADDR` to 127.0.0.1 and that the output will always be
the HTML-based 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=...

Which is how I use it.


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.

The solution scales reasonable well, although adding the keys to the nameserver
configuration is still manual in my setup (but since it does not happen that
often, it's no hassle). This setup has been tested against ISC Bind version 9.


<a name="license">License</a>
-----------------------------
This script, documentation and configration 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, documenatation 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/>.