{"id":1093,"date":"2014-07-02T10:37:02","date_gmt":"2014-07-02T10:37:02","guid":{"rendered":"http:\/\/www.nooblet.org\/blog\/?p=1093"},"modified":"2018-01-06T17:13:25","modified_gmt":"2018-01-06T17:13:25","slug":"self-hosted-dynamic-dns-with-bind9-php","status":"publish","type":"post","link":"https:\/\/www.nooblet.org\/blog\/2014\/self-hosted-dynamic-dns-with-bind9-php\/","title":{"rendered":"Self-Hosted Dynamic DNS with BIND9 &#038; PHP"},"content":{"rendered":"<p>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 <a href=\"http:\/\/www.noip.com\/\" target=\"_blank\">no-ip.com<\/a> domains have been <a href=\"http:\/\/yro.slashdot.org\/story\/14\/07\/01\/0025220\/microsoft-takes-down-no-ipcom-domains\" target=\"_blank\">ceased by US courts<\/a> and <a href=\"http:\/\/www.noip.com\/blog\/2014\/06\/30\/ips-formal-statement-microsoft-takedown\/\" target=\"_blank\">handed over to Microsoft<\/a> means I felt relieved I was now running my own system for some time now. And so could you.<\/p>\n<p>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.<\/p>\n<p>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.<\/p>\n<p>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&#8217;t see the correct IP.<\/p>\n<p>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.<\/p>\n<p><b>Creating DNSSEC keys<\/b><br \/>\nFirst you will need to create a set of DNSSEC keys for the 2 systems to authenticate with.<\/p>\n<pre class=\"lang:default decode:true \" >dnssec-keygen -r \/dev\/urandom -a HMAC-MD5 -b 512 -n HOST web1.example.com<\/pre>\n<p><span style=\"font-size: 0.85em\"><em>Note: Using &#8220;-r \/dev\/urandom&#8221; 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.<\/em><\/span><\/p>\n<p>You will then have 2 new files, in my case <code>Kweb1.example.com.+165+60641.key<\/code> and <code>Kweb1.example.com.+165+60641.private<\/code><\/p>\n<p>In the .private file there is a field &#8220;key&#8221;:<\/p>\n<p><em>Kweb1.example.com.+165+60641.private<\/em><\/p>\n<pre class=\"lang:default highlight:0 decode:true \" >Private-key-format: v1.3\r\nAlgorithm: 165 (HMAC_SHA512)\r\nKey: f3QY8vTQwgX0mo\/7hYUR9m5Vw+X3GsKmRLp850vPTOtgzLmGmIokB9Q1MxYk76CTmVkqPlIyMQxpwizfrLgHsA==\r\nBits: AAA=\r\nCreated: 20140702092344\r\nPublish: 20140702092344\r\nActivate: 20140702092344<\/pre>\n<p><b>Adding BIND config<\/b><br \/>\nYou 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<\/p>\n<p>Add to \/etc\/bind\/named.conf<\/p>\n<pre class=\"lang:default highlight:0 decode:true \" >include \"\/etc\/bind\/named.conf.dyndns\";<\/pre>\n<p>Create \/etc\/bind\/named.conf.dyndns<\/p>\n<pre class=\"lang:default highlight:0 decode:true \" >Create \/etc\/bind\/named.conf.dyndns\r\n\/\/ key used by web1.example.com\r\nkey \"web1.example.com\" {\r\n   algorithm hmac-md5;\r\n   secret \"f3QY8vTQwgX0mo\/7hYUR9m5Vw+X3GsKmRLp850vPTOtgzLmGmIokB9Q1MxYk76CTmVkqPlIyMQxpwizfrLgHsA==\";\r\n};\r\n\r\nzone \"dyndns.example.com\" {\r\n   type master;\r\n   file \"\/etc\/bind\/db\/dyndns.example.com\";\r\n   allow-update {\r\n      key \"web1.example.com\";\r\n   };\r\n};<\/pre>\n<p><span style=\"font-size: 0.85em\"><em>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)<\/em><\/span><br \/>\n<b>Example zone file<\/b><br \/>\nYou will need to start your zonefile with the basics.<\/p>\n<p>Create \/etc\/bind\/db\/dyndns.example.com<\/p>\n<pre class=\"lang:default highlight:0 decode:true \" >$TTL 86400      ; 1 day\r\n@                IN SOA  ns1.example.com. hostmaster.example.com. (\r\n                                2014070101 ; serial\r\n                                3600       ; refresh (1 hour)\r\n                                600        ; retry (10 minutes)\r\n                                2419200    ; expire (4 weeks)\r\n                                3600       ; minimum (1 hour)\r\n                                )\r\n                        NS      ns1.example.com.\r\n                        NS      ns2.example.com.\r\n                        A       192.168.0.1\r\n$ORIGIN @\r\n$TTL 60 ; 1 minute<\/pre>\n<p><b>Setting up the web server<\/b><br \/>\nThis code relies on the program nsupdate. On Debian this is available in the <code>dnsutils<\/code> package. On Redhat based systems it is in the <code>bind-utils<\/code> package.<\/p>\n<p>The PHP code has settings and user authentication in the first 2 arrays, <code>$settings<\/code> and <code>$acl<\/code>.<\/p>\n<pre class=\"lang:php decode:true \" >\/\/ Various settings\r\n$settings = array(\r\n\t\"domain\"     =&gt; \"dyndns.example.com\",\r\n\t\"nsupdate\"   =&gt; \"\/usr\/bin\/nsupdate\",\r\n\t\"nameserver\" =&gt; \"192.168.0.1\",\r\n\t\"keyfile\"    =&gt; \"include\/Kweb1.example.com.+165+60641.private\",\r\n\t\"ttl\"        =&gt; \"60\",\r\n\t\"logdir\"     =&gt; \"log\/\"\r\n);\r\n\r\n\/\/ Access Control List\r\n$acl = array(\r\n\t\"customer1\"  =&gt; \"YRnog2nMaXyzumya2VQX\",\r\n\t\"customer2\"  =&gt; \"27iAsCPAqdVHGf3CVGa4\",\r\n);<\/pre>\n<p>The array <code>$acl<\/code> 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).<\/p>\n<p>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&#038;key=YRnog2nMaXyzumya2VQX)<\/p>\n<p>The script needs access to the key files generated earlier. I placed them in a new directory <code>include\/<\/code>.<\/p>\n<p>The <code>ttl<\/code> 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.<\/p>\n<p>The script will attempt to create a <code>log\/<\/code> directory. If your webserver doesn&#8217;t have permission to do this, you would need to do it manually and give the webserver write permission.<\/p>\n<p>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.<br \/>\n<em>.htaccess<\/em> <\/p>\n<pre class=\"lang:default highlight:0 decode:true \" >Order Allow,Deny\r\nDeny from all<\/pre>\n<p>The script logs failed requests and successful updates to log\/access_YEARMONTH.log<\/p>\n<pre class=\"lang:default highlight:0 decode:true \" >[Tue, 01 Jul 14 11:11:53 +0100] (x.x.x.x) Success: customer1\r\n[Tue, 01 Jul 14 14:15:57 +0100] (x.x.x.x) Forbidden: \/update.php?id=customer1&amp;key=test\r\n[Tue, 01 Jul 14 14:16:08 +0100] (x.x.x.x) Success: customer2<\/pre>\n<p><b>Automatic Updates on Linux\/OSX<\/b><br \/>\nBecause 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.<\/p>\n<p>You can  make this automatic by adding an entry to your systems crontab that will visit the link for you on a regular basis.<br \/>\n<em>crontab<\/em> <\/p>\n<pre class=\"lang:default highlight:0 decode:true \" >*\/30 * * * * wget -qq --no-check-certificate 'https:\/\/web1.example.com\/update.php?id=customer1&amp;key=YRnog2nMaXyzumya2VQX'<\/pre>\n<p><span style=\"font-size: 0.85em\"><em>Note: It is important to include the link in quotes as the &amp; symbol has a special meaning on some shells.<\/em><\/span><\/p>\n<p><b>Automatic Updates on Windows<\/b><br \/>\nFor 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.<\/p>\n<img decoding=\"async\" src=\"\/blog\/wp-content\/plugins\/wp-downloadmanager\/images\/ext\/unknown.gif\" alt=\"\" title=\"\" style=\"vertical-align: middle;\" \/>&nbsp;&nbsp;<strong><a href=\"https:\/\/www.nooblet.org\/blog\/download\/dynamic-dns.vbs\">dynamic-dns.vbs<\/a><\/strong> (478 bytes, 3,944 hits)<br>\n<pre class=\"lang:vb decode:true \" >' This is the client counterpart to a Dynamic DNS system\r\n' Steve Allison 2014 -- http:\/\/www.nooblet.org\/blog\/2014\/php-dynamic-dns\/\r\n\r\nSub fetchURL()\r\n  Set objHTTP = CreateObject(\"WinHttp.WinHttpRequest.5.1\")\r\n  objHTTP.Open \"GET\", strURL, False\r\n  objHTTP.Send\r\nEnd Sub\r\n\r\nDim objHTTP, strURL, strID, strKey\r\n\r\nstrID = \"customer1\"\r\nstrKey = \"YRnog2nMaXyzumya2VQX\"\r\nstrURL = \"https:\/\/web1.example.com\/update.php?id=\" + strID + \"&amp;key=\" + strKey\r\n\r\nOn Error Resume Next\r\nfetchURL<\/pre>\n<p><b>The PHP code<\/b><\/p>\n<img decoding=\"async\" src=\"\/blog\/wp-content\/plugins\/wp-downloadmanager\/images\/ext\/php.gif\" alt=\"\" title=\"\" style=\"vertical-align: middle;\" \/>&nbsp;&nbsp;<strong><a href=\"https:\/\/www.nooblet.org\/blog\/download\/update.php\">update.php<\/a><\/strong> (3.8 KiB, 8,576 hits)<br>\n<pre class=\"lang:php decode:true \" title=\"Dynamic DNS\" >&lt;?php\r\n\r\n\/**\r\n * Dynamic DNS update script\r\n *\r\n * This script takes a username and password and uses the BIND command \"nsupdate\"\r\n * to update a BIND installation with the remote IP of the request\r\n *\r\n * Steve Allison 2014 -- http:\/\/www.nooblet.org\/blog\/2014\/php-dynamic-dns\/\r\n *\r\n**\/\r\n\r\n\/\/ Various settings\r\n$settings = array(\r\n\t\"domain\"\t=&gt; \"dyndns.example.com\",\r\n\t\"nsupdate\"\t=&gt; \"\/usr\/bin\/nsupdate\",\r\n\t\"nameserver\"\t=&gt; \"192.168.0.1\",\r\n\t\"keyfile\"\t=&gt; \"include\/Kweb1.example.com.+165+60641.private\",\r\n\t\"ttl\"\t\t=&gt; \"60\",\r\n\t\"logdir\"\t=&gt; \"log\/\"\r\n);\r\n\r\n\/\/ Access Control List\r\n$acl = array(\r\n\t\"customer1\"\t\t=&gt; \"YRnog2nMaXyzumya2VQX\",\r\n\t\"customer2\"\t\t=&gt; \"27iAsCPAqdVHGf3CVGa4\",\r\n\t\"customer2\"\t\t=&gt; \"tdo2WHLAi454xwMLwOA6\",\r\n\t\"customer4\"\t\t=&gt; \"4AAkBgMCvyig4yGTtAyD\",\r\n);\r\n\r\n\r\n\/\/ Removes unwanted characters from the GET request, probably malicious\r\nfunction escapeString($string) {\r\n\treturn strtr($string, \r\n\t\tarray(\r\n\t\t\t\";\" =&gt; '_',\r\n\t\t\t\",\" =&gt; '_',\r\n\t\t\t\"\\n\" =&gt; '_',\r\n\t\t\t\"\\r\" =&gt; '_',\r\n\t\t\t\"\\\\\" =&gt; '_',\r\n\t\t)\r\n\t);\r\n}\r\n\r\n\/\/ Logs string to file, creates logdir with .htaccess file if necessary\r\nfunction logString($string) {\r\n\r\n\tglobal $settings, $_SERVER;\r\n\r\n\tif (!file_exists($settings['logdir'])) {\r\n\t\tmkdir($settings['logdir']);\r\n\t\tfile_put_contents($settings['logdir'] . \"\/.htaccess\", \"order allow,deny\\ndeny from all\\n\");\r\n\t}\r\n\r\n\tif (file_exists($settings['logdir'])) {\r\n\t\tfile_put_contents($settings['logdir'] . \"\/access_\" . date('Ym') . \".log\", sprintf(\"[%s] (%15s) %s\\n\", date(DATE_RFC822), $_SERVER['REMOTE_ADDR'], $string), FILE_APPEND);\r\n\t}\r\n}\r\n\r\n\r\n\/\/ Check we have the arrays available, or die a quick death\r\nif ((!isset($_GET)) || (!isset($_SERVER)) || (!array_key_exists('REMOTE_ADDR', $_SERVER))) {\r\n\theader('HTTP\/1.0 500 Internal Server Error');\r\n\tprint(\"500 Internal Server Error\\n\");\r\n\tdie();\r\n}\r\n\r\n\/\/ Get remote IP\r\n$remoteip = $_SERVER['REMOTE_ADDR'];\r\n\r\n\/\/ Check that all variables are available to PHP\r\nif ((!array_key_exists('id', $_GET)) || (!array_key_exists('key', $_GET))) {\r\n        header('HTTP\/1.0 400 Bad Request');\r\n        print(\"400 Bad Request\\n\");\r\n\t\tlogString(\"Bad request (required GET field missing): \" . $_SERVER['REQUEST_URI']);\r\n        die();\r\n}\r\n\r\n\/\/ define our 2 main variables\r\n$id = escapeString($_GET['id']);\r\n$key = escapeString($_GET['key']);\r\n\r\n\/\/ If any are null, die\r\nif ((!$id) || (!$key)) {\r\n\theader('HTTP\/1.0 400 Bad Request');\r\n\tprint(\"400 Bad Request\\n\");\r\n\tlogString(\"Bad request (required GET field empty): \" . $_SERVER['REQUEST_URI']);\r\n\tdie();\r\n}\r\n\r\n\/\/ If there is no entry, or the key doesn't match, die\r\nif ((!$acl[$id]) || (strcmp($acl[$id],$key)) != 0) {\r\n\theader('HTTP\/1.0 403 Forbidden');\r\n\tprint(\"403 Forbidden\");\r\n\tlogString(\"Forbidden: \" . $_SERVER['REQUEST_URI']);\r\n\tdie();\r\n}\r\n\r\n\/\/ Perform DNS request to fetch current IP, the extra '.' is to disable appending local domain to request\r\n$currentip = gethostbyname($id . \".\" . $settings['domain'] . \".\");\r\n\r\n\/\/ Check if an update is required, if not, die\r\nif (strcmp($currentip,$remoteip) == 0) {\r\n\theader(\"Content-Type: text\/plain\");\r\n\tprint(\"No update required.\");\r\n\tdie();\r\n}\r\n\r\n\/\/ Check nsupdate exists\r\nif (!file_exists($settings['nsupdate'])) {\r\n\theader('HTTP\/1.0 500 Internal Server Error');\r\n\tprint(\"500 Internal Server Error\");\r\n\tlogString(\"Error: \" . $settings['nsupdate'] . \" is not a valid nsupdate binary\");\r\n\tdie();\r\n}\r\n\r\n\/\/ Run nsupdate\r\n$pipe = popen($settings['nsupdate'] . \" -d -D -k \" . $settings['keyfile'], 'w');\r\n\/\/ Pass update string\r\nfwrite($pipe, \"server \" . $settings['nameserver'] . \"\\n\");\r\n\/\/fwrite($pipe, \"debug yes\\n\");\r\nfwrite($pipe, \"zone \" . $settings['domain'] . \"\\n\");\r\nfwrite($pipe, \"update delete \" . $id . \".\" . $settings['domain'] . \" A\\n\");\r\nfwrite($pipe, \"update add \" . $id . \".\" . $settings['domain'] . \" \" . $settings['ttl'] . \" A \" . $remoteip . \"\\n\");\r\n\/\/fwrite($pipe, \"show\\n\");\r\nfwrite($pipe, \"send\\n\");\r\n\/\/ Close pipe\r\n$int = pclose($pipe);\r\n\r\n\/\/ log to file\r\nlogString(\"Success: \" . $id);\r\n\r\nheader(\"Content-Type: text\/plain\");\r\nprint(\"Request submitted.\\n\" . $id . \" =&gt; \" . $remoteip . \"\\n\");\r\n\r\n<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>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 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":648,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5],"tags":[227,228,229,230,231],"class_list":["post-1093","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-linux","tag-bind","tag-bind9","tag-dns","tag-dynamic-dns","tag-php"],"_links":{"self":[{"href":"https:\/\/www.nooblet.org\/blog\/wp-json\/wp\/v2\/posts\/1093","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.nooblet.org\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.nooblet.org\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.nooblet.org\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.nooblet.org\/blog\/wp-json\/wp\/v2\/comments?post=1093"}],"version-history":[{"count":33,"href":"https:\/\/www.nooblet.org\/blog\/wp-json\/wp\/v2\/posts\/1093\/revisions"}],"predecessor-version":[{"id":1128,"href":"https:\/\/www.nooblet.org\/blog\/wp-json\/wp\/v2\/posts\/1093\/revisions\/1128"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.nooblet.org\/blog\/wp-json\/wp\/v2\/media\/648"}],"wp:attachment":[{"href":"https:\/\/www.nooblet.org\/blog\/wp-json\/wp\/v2\/media?parent=1093"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.nooblet.org\/blog\/wp-json\/wp\/v2\/categories?post=1093"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.nooblet.org\/blog\/wp-json\/wp\/v2\/tags?post=1093"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}