self hosted dynamic dns using bind9 (named) and openresty (nginx)

First step is to configure your domain properly. My domain provider (gandi) let me edit my zone file, so I put in it the following content:

@ 10800 IN A 1.2.3.4
ns 10800 IN A 1.2.3.4
* 10800 IN CNAME example.org.
*.ns 10800 IN NS ns
    

The first line tells to get the ip of my vps server when asking the dns record for example.org; the second line do the same for ns.example.org; the third line says to get *.example.org pointing to the same address of the bare domain (CNAME is like an alias); the last line is the most interesting: the NS record says that *.ns.example.org will be resolved by the dns server ns.example.org. 1.2.3.4 is a placeholder for the ip address of my vps server. Depending on the provider you may have to wait minutes to hours before the edit will propagate.

The next step is to install and start bind9 on your server; refer to your OS manual to get the proper software running. Once I got it up and running on port 53 I opened the /etc/named.conf file and added my zone section:

zone "example.org" {
  type master;
  file "dnsdb.example.org";
  allow-update { localhost; };
};
    

With this configuration I told bind9 to get the zone information in /var/named/dnsdb.example.org and that dns update requests are accepted only if originating on localhost.

Next step is to create the zone file; I copied the empty.zone file and edited the relevant bits, and added a record for ns.

$ORIGIN example.org.
$TTL 1h

@ 1D IN SOA example.org. root.example.org. (
  2016120401 ; serial
  1H ; refresh
  15M ; retry
  1W ; expiry
  1D ) ; min ttl
    1D IN NS ns.example.org.

ns.example.org. 1D IN A 127.0.0.1
    

After restarting named I tested the configuration inserting a subdomain using nsupdate.

$ cat > add_loopback.txt <<EOF
server localhost
zone example.org
update add loopback.ns.example.org 3600 A 127.0.0.1
show
send
EOF
$ nsupdate add_loopback.txt
Outgoing update query:
;; ->>HEADER<<- opcode: UPDATE, status: NOERROR, id:      0
;; flags:; ZONE: 0, PREREQ: 0, UPDATE: 0, ADDITIONAL: 0
;; ZONE SECTION:
;example.org			IN	SOA

;;UPDATE SECTION:
loopback.ns.example.org.	3600	IN	A	127.0.0.1

$
    

I checked that the result was public:

$ ping loopback.ns.example.org
PING loopback.ns.example.org (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=1 ttl=64 time=0.026 ms
^C
--- loopback.ns.example.org ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.026/0.026/0.026/0.000 ms
    

Next step was getting openresty up and running on my server; again, refer to your OS manuals and documentation.

I edited the openresty's nginx.conf. In my distribution it is /opt/openresty/nginx/conf/nginx.conf.

  [...]
  server {
    listen 80;
    server_name ns.example.org;
    location /nsadmin {
      content_by_lua_file conf/nsadmin.lua;
    }
  }
  [...]
    

I created the following nsadmin.lua script in the same directory of nginx.conf file:

local key = ngx.var.arg_key
local domain = ngx.var.arg_domain
local ip = ngx.var.arg_ip

fd = io.open("/opt/openresty/nginx/conf/credentials.txt")
for line in fd:lines() do
  local lkey, ldomain = line:match("^([^:]*):([^:]*)$")
  if key == lkey and domain == ldomain then
    if ip == nil then
      break
    end
    ngx.log(ngx.DEBUG, "updating: k=" .. key .. " d=" .. domain .. " i=" .. ip)
    local cmd = 'server localhost\n'
    cmd = cmd .. 'zone example.org\n'
    cmd = cmd .. 'update delete ' .. domain .. '.ns.example.org A\n'
    cmd = cmd .. 'update add ' .. domain .. '.ns.example.org 3600 A ' .. ip .. '\n'
    cmd = cmd .. 'show\n'
    cmd = cmd .. 'send\n'
    ngx.say('nsupdate command:\n' .. cmd)
    p = io.popen("echo '" .. cmd .. "' | nsupdate 2>&1")
    for pline in p:lines() do
      ngx.say(pline .. "\n")
    end
    p.close()
    return
  end
end
fd:close()

ngx.say("unauthorized")
    

The credentials.txt file content is simple: one key:domain pair per line:

mysecretpassword:mysubdomainname
    

And finally after restarting openresty the server configuration is complete.

On the client side I put in a cron job the update of the relevant ip address (in my case it was the internal network address of a wifi device, in a network where I can't get access to the dhcp administration):

      curl -v 'http://ns.example.org/nsadmin?key=mysecretpassword&domain=mysubdomainname&ip='$(ifdata -pa wlan0)
    

A handy windows .bat to update the domain ip address:

      @if (@This==@IsBatch) @then
	wscript //E:JScript "%~dpnx0"
	exit /b
      @end
      var http = WScript.CreateObject('Msxml2.XMLHTTP.6.0');
      http.open('GET','http://icanhazip.com', false);
      http.setRequestHeader('User-Agent','curl');
      http.send();
      var externalIP = http.responseText;
      var updateUrl = "http://ns.example.org/nsadmin?key=mysecretpassword&domain=mysubdomainname&ip=" + externalIP;
      http = WScript.CreateObject('Msxml2.XMLHTTP.6.0');
      http.open('GET', updateUrl, false);
      http.setRequestHeader('User-Agent','curl');
      http.send();
      WScript.Echo(http.responseText);
      WScript.Quit(0);