There are several free Dynamic DNS services available, but the ones I have used require the user to respond to an email every 30-days to confirm the account is still in use. DynDns no longer offer free accounts, and some recent news that no-ip.com domains have been ceased by US courts and handed over to Microsoft means I felt relieved I was now running my own system for some time now. And so could you.
This system uses BIND9 to host the DNS and PHP to handle the update requests. Setting up BIND and Apache/Nginx/PHP is outside the scope of this guide.
A user updates their IP by visiting a unique link. In the guide you will find methods of automating dns updates with Linux, OSX and Windows.
I suggest hosting the script on HTTPS or a non-standard port as some Internet Providers (I know Virgin does) use transparent cache proxies for web traffic meaning the web server doesn’t see the correct IP.
In my examples I am creating a domain named dyndns.example.com and have a webserver at web1.example.com, and a nameserver at ns1.example.com. Guide is based on Debian Wheezy, but should be distribution independent.
Creating DNSSEC keys
First you will need to create a set of DNSSEC keys for the 2 systems to authenticate with.
1 |
dnssec-keygen -r /dev/urandom -a HMAC-MD5 -b 512 -n HOST web1.example.com |
Note: Using “-r /dev/urandom” tells the command to use the less secure non-blocking random generator. Without it, you may find the command blocks until enough random entropy has been gathered to generate the keys.
You will then have 2 new files, in my case Kweb1.example.com.+165+60641.key
and Kweb1.example.com.+165+60641.private
In the .private file there is a field “key”:
Kweb1.example.com.+165+60641.private
1 2 3 4 5 6 7 |
Private-key-format: v1.3 Algorithm: 165 (HMAC_SHA512) Key: f3QY8vTQwgX0mo/7hYUR9m5Vw+X3GsKmRLp850vPTOtgzLmGmIokB9Q1MxYk76CTmVkqPlIyMQxpwizfrLgHsA== Bits: AAA= Created: 20140702092344 Publish: 20140702092344 Activate: 20140702092344 |
Adding BIND config
You then need to add this key to BIND and create the zone config. I personally create a new file /etc/bind/named.conf.dyndns and add an extra include directive in /etc/bind/named.conf
Add to /etc/bind/named.conf
1 |
include "/etc/bind/named.conf.dyndns"; |
Create /etc/bind/named.conf.dyndns
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Create /etc/bind/named.conf.dyndns // key used by web1.example.com key "web1.example.com" { algorithm hmac-md5; secret "f3QY8vTQwgX0mo/7hYUR9m5Vw+X3GsKmRLp850vPTOtgzLmGmIokB9Q1MxYk76CTmVkqPlIyMQxpwizfrLgHsA=="; }; zone "dyndns.example.com" { type master; file "/etc/bind/db/dyndns.example.com"; allow-update { key "web1.example.com"; }; }; |
Note: I point zonefiles to /etc/bind/db/, either edit the location or create the directory (remember to give bind write permissions to this directory)
Example zone file
You will need to start your zonefile with the basics.
Create /etc/bind/db/dyndns.example.com
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$TTL 86400 ; 1 day @ IN SOA ns1.example.com. hostmaster.example.com. ( 2014070101 ; serial 3600 ; refresh (1 hour) 600 ; retry (10 minutes) 2419200 ; expire (4 weeks) 3600 ; minimum (1 hour) ) NS ns1.example.com. NS ns2.example.com. A 192.168.0.1 $ORIGIN @ $TTL 60 ; 1 minute |
Setting up the web server
This code relies on the program nsupdate. On Debian this is available in the dnsutils
package. On Redhat based systems it is in the bind-utils
package.
The PHP code has settings and user authentication in the first 2 arrays, $settings
and $acl
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// Various settings $settings = array( "domain" => "dyndns.example.com", "nsupdate" => "/usr/bin/nsupdate", "nameserver" => "192.168.0.1", "keyfile" => "include/Kweb1.example.com.+165+60641.private", "ttl" => "60", "logdir" => "log/" ); // Access Control List $acl = array( "customer1" => "YRnog2nMaXyzumya2VQX", "customer2" => "27iAsCPAqdVHGf3CVGa4", ); |
The array $acl
is made up of 2 fields. These are used as the ID and KEY for authentication. The ID is the subdomain to be updated (ie. customer1.dyndns.example.com).
In order to authenticate and update the IP, a user only needs to visit the page with the correct details. (ie. https://web1.example.com/update.php?id=customer1&key=YRnog2nMaXyzumya2VQX)
The script needs access to the key files generated earlier. I placed them in a new directory include/
.
The ttl
setting determines how long the internet should cache each DNS entry. A lower TTL would make changes propagate quicker, but would increase the number of requests for busy entries.
The script will attempt to create a log/
directory. If your webserver doesn’t have permission to do this, you would need to do it manually and give the webserver write permission.
You should deny public access to the include and log directories. If using Apache, you can add a .htaccess file to each directory to deny access.
.htaccess
1 2 |
Order Allow,Deny Deny from all |
The script logs failed requests and successful updates to log/access_YEARMONTH.log
1 2 3 |
[Tue, 01 Jul 14 11:11:53 +0100] (x.x.x.x) Success: customer1 [Tue, 01 Jul 14 14:15:57 +0100] (x.x.x.x) Forbidden: /update.php?id=customer1&key=test [Tue, 01 Jul 14 14:16:08 +0100] (x.x.x.x) Success: customer2 |
Automatic Updates on Linux/OSX
Because this system just visits a URL to update the IP, there is no need for special software. All you need to do is visit the link.
You can make this automatic by adding an entry to your systems crontab that will visit the link for you on a regular basis.
crontab
1 |
*/30 * * * * wget -qq --no-check-certificate 'https://web1.example.com/update.php?id=customer1&key=YRnog2nMaXyzumya2VQX' |
Note: It is important to include the link in quotes as the & symbol has a special meaning on some shells.
Automatic Updates on Windows
For Windows I have written a small VBScript program that can be ran by a scheduled task. The script has been tested on XP/Vista/7/8.
dynamic-dns.vbs (478 bytes, 3,190 hits)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
' This is the client counterpart to a Dynamic DNS system ' Steve Allison 2014 -- http://www.nooblet.org/blog/2014/php-dynamic-dns/ Sub fetchURL() Set objHTTP = CreateObject("WinHttp.WinHttpRequest.5.1") objHTTP.Open "GET", strURL, False objHTTP.Send End Sub Dim objHTTP, strURL, strID, strKey strID = "customer1" strKey = "YRnog2nMaXyzumya2VQX" strURL = "https://web1.example.com/update.php?id=" + strID + "&key=" + strKey On Error Resume Next fetchURL |
The PHP code
update.php (3.8 KiB, 8,576 hits)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
<?php /** * Dynamic DNS update script * * This script takes a username and password and uses the BIND command "nsupdate" * to update a BIND installation with the remote IP of the request * * Steve Allison 2014 -- http://www.nooblet.org/blog/2014/php-dynamic-dns/ * **/ // Various settings $settings = array( "domain" => "dyndns.example.com", "nsupdate" => "/usr/bin/nsupdate", "nameserver" => "192.168.0.1", "keyfile" => "include/Kweb1.example.com.+165+60641.private", "ttl" => "60", "logdir" => "log/" ); // Access Control List $acl = array( "customer1" => "YRnog2nMaXyzumya2VQX", "customer2" => "27iAsCPAqdVHGf3CVGa4", "customer2" => "tdo2WHLAi454xwMLwOA6", "customer4" => "4AAkBgMCvyig4yGTtAyD", ); // Removes unwanted characters from the GET request, probably malicious function escapeString($string) { return strtr($string, array( ";" => '_', "," => '_', "\n" => '_', "\r" => '_', "\\" => '_', ) ); } // Logs string to file, creates logdir with .htaccess file if necessary function logString($string) { global $settings, $_SERVER; if (!file_exists($settings['logdir'])) { mkdir($settings['logdir']); file_put_contents($settings['logdir'] . "/.htaccess", "order allow,deny\ndeny from all\n"); } if (file_exists($settings['logdir'])) { file_put_contents($settings['logdir'] . "/access_" . date('Ym') . ".log", sprintf("[%s] (%15s) %s\n", date(DATE_RFC822), $_SERVER['REMOTE_ADDR'], $string), FILE_APPEND); } } // Check we have the arrays available, or die a quick death if ((!isset($_GET)) || (!isset($_SERVER)) || (!array_key_exists('REMOTE_ADDR', $_SERVER))) { header('HTTP/1.0 500 Internal Server Error'); print("500 Internal Server Error\n"); die(); } // Get remote IP $remoteip = $_SERVER['REMOTE_ADDR']; // Check that all variables are available to PHP if ((!array_key_exists('id', $_GET)) || (!array_key_exists('key', $_GET))) { header('HTTP/1.0 400 Bad Request'); print("400 Bad Request\n"); logString("Bad request (required GET field missing): " . $_SERVER['REQUEST_URI']); die(); } // define our 2 main variables $id = escapeString($_GET['id']); $key = escapeString($_GET['key']); // If any are null, die if ((!$id) || (!$key)) { header('HTTP/1.0 400 Bad Request'); print("400 Bad Request\n"); logString("Bad request (required GET field empty): " . $_SERVER['REQUEST_URI']); die(); } // If there is no entry, or the key doesn't match, die if ((!$acl[$id]) || (strcmp($acl[$id],$key)) != 0) { header('HTTP/1.0 403 Forbidden'); print("403 Forbidden"); logString("Forbidden: " . $_SERVER['REQUEST_URI']); die(); } // Perform DNS request to fetch current IP, the extra '.' is to disable appending local domain to request $currentip = gethostbyname($id . "." . $settings['domain'] . "."); // Check if an update is required, if not, die if (strcmp($currentip,$remoteip) == 0) { header("Content-Type: text/plain"); print("No update required."); die(); } // Check nsupdate exists if (!file_exists($settings['nsupdate'])) { header('HTTP/1.0 500 Internal Server Error'); print("500 Internal Server Error"); logString("Error: " . $settings['nsupdate'] . " is not a valid nsupdate binary"); die(); } // Run nsupdate $pipe = popen($settings['nsupdate'] . " -d -D -k " . $settings['keyfile'], 'w'); // Pass update string fwrite($pipe, "server " . $settings['nameserver'] . "\n"); //fwrite($pipe, "debug yes\n"); fwrite($pipe, "zone " . $settings['domain'] . "\n"); fwrite($pipe, "update delete " . $id . "." . $settings['domain'] . " A\n"); fwrite($pipe, "update add " . $id . "." . $settings['domain'] . " " . $settings['ttl'] . " A " . $remoteip . "\n"); //fwrite($pipe, "show\n"); fwrite($pipe, "send\n"); // Close pipe $int = pclose($pipe); // log to file logString("Success: " . $id); header("Content-Type: text/plain"); print("Request submitted.\n" . $id . " => " . $remoteip . "\n"); |