分享

HOW-TO: be your own DDNS provider

 Dead n Gone 2014-09-22

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";
 
  // 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
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.

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

This blog post was created on 2011-01-31 at 20:06 and last modified on 2014-01-11 at 14:25 by Andrwe Lord Weber.
It is tagged with .

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:///dynamic-dns/

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

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

2 |
sam
| 2012/03/19 07: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 21:36 | reply

Thanks for the very useful instructions – I used them for my own similiar solution here : http:///blog/2013/07/08/dyndns-for-bind/

4 |
Max Baker
| 2013/07/09 02:45 | 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.

5 |
Michael
| 2013/08/30 15: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.

6 |
Andrwe Lord Weber
| 2014/01/11 14: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./articles/running-a-secure-ddns-service-with-bind/

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

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多