Benutzer-Werkzeuge

Webseiten-Werkzeuge


linux:own-ddns

HOW-TO: be your own DDNS provider

Summary

This how-to explains a way to build your own dynamic DNS server.

Main

Hi, since some time my DDNS provider has problems which cause the loss of the connection to my home server.
To prevent this loss I've read some manpages and build my own DDNS server.

:!: This is not a how-to about DNS, bind or any other software I've used. :!:

Warranty

There is no warranty that this how-to works for any system.
I'm just providing these information because it worked for me this way.
If you have questions you can leave a message here but I decide whether I'll answer and help or not.

Requirements:

  • own server with static IP
  • own domain resolving (e.g.: example.org)
  • subdomain delegated to to your server (e.g.: dyndns.example.org)
  • php5
  • webserver supporting PHP (I use Lighttpd, but any will do)
  • bind>=9
  • dnsutils

Configurations

You have to change all 'dyndns.example.org' to your domain.

bind

  1. the bind user requires write-access to bind working directory:
    // named.conf
    options {
      ...
      ; working directory of bind
      directory "/var/named";
      ...
    };
    chmod 770 /var/named/
  2. we generate a TSIG-key in a new directory which is used to verify the server and client:
    mkdir -p /etc/named/
    cd /etc/named/
    dnssec-keygen -a hmac-sha512 -b 512 -n HOST dyndns.example.org
     
    # webserver-group needs read access to file containing TSIG-key
    chown root:<webserver-group> /etc/named/Kdyndns.example.org*.private
    chmod 640 /etc/named/Kdyndns.example.org*.private
     
    # get and remember the key
    grep Key /etc/named/Kdyndns.example.org*.private
  3. create zone in named.conf
    key mykey {
      algorithm hmac-sha512;
      secret "the-generated-key";
    };
     
    zone "dyndns.example.org" IN {
      type master;
      file "dyndns.example.org.zone";
      allow-query { any; };
      allow-transfer { none; };
      allow-update { key mykey; };
    };
  4. create zone-file
    /var/named/dyndns.example.org.zone
    $ORIGIN .
    $TTL 86400  ; 1 day
    dyndns.example.org    IN SOA  localhost. root.localhost. (
            52         ; serial
            3600       ; refresh (1 hour)
            900        ; retry (15 minutes)
            604800     ; expire (1 week)
            86400      ; minimum (1 day)
            )
          NS  localhost.
    $ORIGIN dyndns.example.org.

Webserver

Create a subdomain (dyndns.example.org) and a vhost for the updating script.
For security purpose and compatibility of the php-script the vhost has to be protected by http-authentication.
For Lighttpd you can use the script provided here to generate the users.
Save this PHP-script in the vhost-directory:

index.php
<?php
  // configuration of user and domain
  $user_domain = array( 'user' => array('subdomain','sub2'), 'user2' => array('sub4') );
  // main domain for dynamic DNS
  $dyndns = "dyndns.example.org";
  // DNS server to send update to
  $dnsserver = "localhost";
  // port of DNS server
  $dnsport = "";
 
  // short sanity check for given IP
  function checkip($ip)
  {
    $iptupel = explode(".", $ip);
    foreach ($iptupel as $value)
    {
      if ($value < 0 || $value > 255)
        return false;
      }
    return true;
  }
 
  // retrieve IP
  $ip = $_SERVER['REMOTE_ADDR'];
  // retrieve user
  if ( isset($_SERVER['REMOTE_USER']) )
  {
    $user = $_SERVER['REMOTE_USER'];
  }
  else if ( isset($_SERVER['PHP_AUTH_USER']) )
  {
    $user = $_SERVER['PHP_AUTH_USER'];
  }
  else
  {
    syslog(LOG_WARN, "No user given by connection from $ip");
    exit(0);
  }
 
  // open log session
  openlog("DDNS-Provider", LOG_PID | LOG_PERROR, LOG_LOCAL0);
 
  // check for given domain
  if ( isset($_POST['DOMAIN']) )
  {
    $subdomain = $_POST['DOMAIN'];
  }
  else if ( isset($_GET['DOMAIN']) )
  {
    $subdomain = $_GET['DOMAIN'];
  }
  else
  {
    syslog(LOG_WARN, "User $user from $ip didn't provide any domain");
    exit(0);
  }
 
  // check for needed variables
  if ( isset($subdomain) && isset($ip) && isset($user) )
  {
    // short sanity check for given IP
    if ( preg_match("/^(\d{1,3}\.){3}\d{1,3}$/", $ip) && checkip($ip) && $ip != "0.0.0.0" && $ip != "255.255.255.255" )
    {
      // short sanity check for given domain
      if ( preg_match("/^[\w\d-_\*\.]+$/", $subdomain) )
      {
        // check whether user is allowed to change domain
        if ( in_array("*", $user_domain[$user]) or in_array($subdomain, $user_domain[$user]) )
        {
          if ( $subdomain != "-" )
            $subdomain = $subdomain . '.';
          else
            $subdomain = '';
 
          // shell escape all values
          $subdomain = escapeshellcmd($subdomain);
          $user = escapeshellcmd($user);
          $ip = escapeshellcmd($ip);
 
          // prepare command
          $data = "<<EOF
server $dnsserver $dnsport
zone $dyndns
update delete $subdomain$user.$dyndns A
update add $subdomain$user.$dyndns 300 A $ip
send
EOF";
          // run DNS update
          exec("/usr/bin/nsupdate -k /etc/named/K$dyndns*.private $data", $cmdout, $ret);
          // check whether DNS update was successful
          if ($ret != 0)
          {
            syslog(LOG_INFO, "Changing DNS for $subdomain$user.$dyndns to $ip failed with code $ret");
          }
        }
        else
        {
          syslog(LOG_INFO, "Domain $subdomain is not allowed for $user from $ip");
        }
      }
      else
      {
        syslog(LOG_INFO, "Domain $subdomain for $user from $ip with $subdomain was wrong");
      }
    }
    else
    {
      syslog(LOG_INFO, "IP $ip for $user from $ip with $subdomain was wrong");
    }
  }
  else
  {
    syslog(LOG_INFO, "DDNS change for $user from $ip with $subdomain failed because of missing values");
  }
  // close log session
  closelog();
?>

Usage

If you've configured all correctly you can update domains using this command:

wget --no-check-certificate --http-user="user" --http-passwd="password" --post-data "DOMAIN=example" -q https://dyndns.example.com

Some examples:

Script configuration:

$user_domain = array( 'user' => array('subdomain') );
$dyndns = "dyndns.example.org"

Result:
The user 'user' can update the IP for the domain subdomain.user.dyndns.example.org.

Script configuration:

$user_domain = array( 'user' => array('subdomain'), 'user2' => array('test', 'foobar') );
$dyndns = "dyndns.example.org"

Result:
The user 'user' can update the IP for the domain subdomain.user.dyndns.example.org.
The user 'user2' can update the IP for the domains test.user2.dyndns.example.org and foobar.user2.dyndns.example.org.

Script configuration:

$user_domain = array( 'user' => array('*'), 'user2' => array('test', 'foobar') );
$dyndns = "dyndns.example.org"

Result:
The user 'user' can update the IP for the wildcard domain *.user.dyndns.example.org which means all subdomains of user.dyndns.example.org are resolved to the IP set for *.
The user 'user2' can update the IP for the domains test.user2.dyndns.example.org and foobar.user2.dyndns.example.org.

Script configuration:

$user_domain = array( 'user' => array('-','subdomain'), 'user2' => array('test', 'foobar') );
$dyndns = "dyndns.example.org"

Result:
The user 'user' can update the IP for the domains subdomain.user.dyndns.example.org and user.dyndns.example.org.
The user 'user2' can update the IP for the domains test.user2.dyndns.example.org and foobar.user2.dyndns.example.org.

Sources

Comments

Great article! I think the examples would apply to a few other providers with some very minor tweaks as well. When all else fails, you can always use a free service to try to accomplish something similar such as some of the others at http://dnslookup.me/dynamic-dns/

1 |
ddns
| 2011/06/26 16:09 | reply

Pretty good, but if you want to provide user management there are pre made packages like GNUDIP http://gnudip2.sourceforge.net/ and MintDNS http://www.dyndnsservices.com both of which are free for non commercial use.

2 |
sam
| 2012/03/19 06:42 | reply

Hello,

Very cool! I was getting a badkey error that I couldn't get rid of so I performed the following changes:

named.conf allow-update { localhost; };

index.php: shell_exec(„/usr/bin/nsupdate «EOF

I know that this isn't ideal, but I deployed a VM just for Dynamic DNS so it shouldn't be a problem. With that said, I would appreciate any suggestion you have in relation to the badkey error I was receiving.

Regards,

Kooby

3 |
Kooby
| 2013/06/16 19:36 | reply

Nice howto that's pretty easy customizable for ones need.

I tried GnuDIP before this. It is great I guess but only for someone beeing able to use a full fledged ddns client talking to the daemon application. The web interface however is pretty sh** and absolutely not intuitive. Also, you can't update with just a single http get request. That makes it almost unusable for any „stupid“ router. Besides it hasn't been maintained for a decade now.

So I'm really glad I found something simple and straightforward just to fit my needs.

However I find there are some issues with the PHP code. I'm not an PHP master myself I must say, so some of the following may or as well may not be of high quality.

One is the checks for $ip and $subdomain to be standard compliant. I replaced preg_match_all with preg_match as it really does not execute the code in the brackets if the pattern does not match. Also the patterns are not precise enough. For example the $ip is 127.0.0.1 then /(\d{1,3}\.){3}\d{1,3}/ matches, but the string 111111127.0.0.11111111 also matches as there's no start and no end in the pattern. Same with the second pattern.

The easiest way check an IP (no sanity checks „do you really want to set IP 0.0.0.0“ though) that comes to my mind without doing some regex brainfuck is to explode the ip to an array and check the 4 values that way.

function checkip($ip)
{
  $iptupel = explode(".", $ip);
  foreach ($iptupel as $value)
  {
    if ($value < 0 || $value > 255)
      return false;
    }
  return true;
}

Combined with the following regex checks (I prefer posix style character classess…):

if ( preg_match("/^([[:digit:]]{1,3}\.){3}[[:digit:]]{1,3}$/", $ip) && checkip($ip))
{
  if ( preg_match("/^[\w\d-_\.\*]+$/", $subdomain))
  {

2nd thing is the shell_exec and how it is processed. Well, I think using shell commands from PHP are somewhat be a quick but dirty. So at least we should use escapeshellcmd() function for everything that is passed to shell execution that came from outside the script. In your case that is $subdomain, $user and $ip (I regard $dyndns as somewhat safe as it's not set by GET/POST).

The next thing with the shell_exec processing here is that you're not checking if the commands are really processed and worked out well. That's kind of an optimistic approach that ends up in an unnoticed failure once in a while. So I prefer using exec() function as it gives back what the system sais. That way I also discovered, that the second shell_exec miserably fails on my server as www-data is not allowed to reload bind. Well, of course, it isn't supposed to do so anyway. But luckily it seems updates work even with out reloading bind, so I just deleted that statement without subsitution.

Here's the shell command processing code:

$subdomain = escapeshellcmd($subdomain);
$user = escapeshellcmd($user);
$ip = escapeshellcmd($ip);

$data = "<<EOF
zone $dyndns
update delete $subdomain$user.$dyndns A
update add $subdomain$user.$dyndns 300 A $ip
send
EOF";

exec("/usr/bin/nsupdate -k $key $data", $cmdout, $ret);
if ($ret)
{
  //some additional code
  die($ret);
}
else
{
  //some additional code
  die($ret);
}

Or in short just

die($ret);

here as we're not doing any further work (at least not in your script).

Note: Linux systems return „0“ if everything is fine and nonzero on error, contrary to PHP. Therefore the if ($ret) and not if (! $ret). And sadly nsupdate doesn't seem to talk to stdout, so $cmdout will always be empty and the administrator has to check the problem in the dark with just the error code.

4 |
Michael
| 2013/08/30 13:23 | reply

@Michael: Hi Michael,

thanks for your comment. I've added the checks you provided and improved some a bit.

Now there is some error logging to.

5 |
Andrwe Lord Weber
| 2014/01/11 13:27 | reply

Hello,

great article for starting of working with DDNS!. I ran into error too „SIG error with server: tsig indicates error update failed: NOTAUTH(BADKEY)“

It seems to me that your instruction is not clear enough about how to name the key. (I'm using hmac-md5 instead since I use BIND 9) If you create a key with dnssec-keygen -a hmac-md5 -b 256 -n HOST pad022.ped.intra then the key files will become like Kpad022.pad.intra.+157+02662.key Kpad022.pad.intra.+157+02662.private then the key section in named.conf should look like key „ped022.ped.intra.“ { ⇐ the dot at the end is significant here!

 algorithm hmac-md5;
 secret "UNjYtRC83H1suRRwCXaa/qjWc0jiz/z9L72hsTAyytQ=";

}; and the same applies here too zone „ddns.intra“ IN {

type master;
file "master/ddns.intra";
allow-query { any; };
allow-transfer { none; };
allow-update { key "pad022.pad.intra."; };

This setup will alow host pad022.pad.intra to update the zone ddns.intra

Another good example of ddns is found here: http://www.kirya.net/articles/running-a-secure-ddns-service-with-bind/

Thanks for your good start, let's improve as we go. Peter

6 |
Peter
| 2014/01/16 21:02 | reply

For easy IP validation use this built-in function: http://www.php.net/manual/en/filter.filters.validate.php

7 |
tibiajaj
| 2014/04/09 11:20 | reply

Im fighting with it from 2 days.. I'm using apache and bind 9 on centos 7 server. I use Andrwe Lord Weber advise and now when I'm using command /usr/bin/nsupdate -k /etc/named/Kdomain*.private from console of this server I'am able to chenge the dns records. When I'm trying tu use command wget –no-check-certificate –http-user=„user“ –http-passwd=„password“ –post-data „DOMAIN=-“ -q https://dyndns.example.com, I'm see in the messages log this: client 127.0.0.1#38528: update 'domain.name/IN' denied DDNS-Provider[1850]: Changing DNS for dyn.domain.name to 84.xx.xx.xx failed with code 2

Thanks

8 |
Eustachy
| 2015/02/18 14:58 | reply

@Eustachy: I got the same issue and investigated it. I found that it fails because since an upgrade of bind9 you have to provide the server directive within the nsupdate command.

Thus I've updated the script above. I added the variables dnsserver and dnsport to represent the DNS server which should be updated. These variables are used in the commands given to nsupdate.

Please try the new version and get back here if it doesn't work for you.

9 |
Andrwe Lord Weber
| 2015/03/12 19:32 | reply

Hi,

Thank you for the howto. You have typo at line 7 and 9. Ending semicolon is missing.

Regards.

10 |
Erjo
| 2015/04/11 00:08 | reply

@Erjo: Hi, thanks for the hint. I fixed the code.

11 |
Andrwe Lord Weber
| 2015/04/15 22:11 | reply

Hi and thanks for that nice tutorial.

I've been messing around quite some time with your script and similar solutions based on bind/php/whatever to get some domain names for my devices in my home network. All of the solutions out there seemed to complicated to me, so i decided to write my own little dyndns server based on java with a simple update mechanism, a small webinterface and no more messing around with zone files. Maybe you or a visitor is searching for something like this, so here's the link: http://limbomedia.net/limbodns.php

It's free with sources on github, so feel free to try it and let me know if there are things to improve.

12 |
Thomas
| 2015/05/26 22:08 | reply

This really is an excellent guide and does exactly what I'm looking for, thank you!

13 |
Andy
| 2016/01/25 12:12 | reply

Excellent, thanks for this. Have you put it on GitHub yet so I can fork it?

Robbie

14 |
Robbie
| 2016/07/08 15:37 | reply

@Kooby: problem with this IP Check is that it limits the script to ipv4. Can the script be updated to include ipv6?

15 |
Robbie
| 2016/07/12 14:24 | reply

@Robbie: For the sake of others and in case it isn't obvious, my question is rhetorical. Of course it would be best to eliminate the checkip() function and instead use FILTER_VALIDATE_IP.

16 |
Robbie
| 2016/07/12 14:28 | reply

Hi and thanks for that nice tutorial.

Can I try this on my website too? https://www.mysiteplan.com/

17 |
Micheal Brian
| 2017/05/21 13:10 | reply

I'm getting the following error from the nsupdate command running in the php script on Ubuntu 18.04:

23-Jul-2018 21:14:21.483 /home/conf/gilgongo/dyndns/Kdyn.xxx.com.+165+52846.private:1: unknown option 'Private-key-format:' 23-Jul-2018 21:14:21.483 /home/conf/gilgongo/dyndns/Kdyn.xxx.com.+165+52846.private:8: unexpected token near end of file could not read key from /home/conf/gilgongo/dyndns/Kdyn.xxx.com.+165+52846.{private,key}: unexpected token DDNS-Provider[9993]: Changing DNS for pihole.unifi.dyn.xxx.com to 62.102.xxx.xxx failed with code 2

Has the nsupdate format changed again?

18 |
Tommy
| 2018/07/23 20:41 | reply


D E​ R D D
linux/own-ddns.txt · Zuletzt geändert: 2015/04/15 22:11 von Andrwe Lord Weber