For a little while I’ve been running a DIY dynamic DNS service. My peers think it’s convenient and pretty sweet, so in this post I’ll set things straight — a full disclosure on the mess that it really is ;-)
What is a dynamic DNS service?
Since the dial-up days of yore I have occasionaly been using “dynamic DNS” services. What is a “dynamic DNS” service, you ask? Well, it’s a marketing term, not a technical term, but most providers of such services allow for having some CNAME or A record point to an IP, and this pairing is updatable over HTTP. Usually they use a freemium business model where you can get a couple of subdomains on their second level domains.
Why use one?
An example of a use case: You run a development server on your laptop. You want someone to connect to it. Instead of saying to this person “now just go to… errr… hold on” (and now you pop your shell and run ip a s dev wlan0)1 “yes I’m back, you need to go to 110.34.56.250″ you can just say “go to kleinebeer.ath.cx”. Much easier. And that’s with IPv4. Typing an IPv6 address that someone just yelled at you from across the room is an even less joyful experience.
To enable this use case you configured your machine in such a way that every time your interface aquires a new IP, a program contacts the “dynamic DNS service” to update the ‘kleinebeer’ DNS record to reflect this IP. There are many clients for such services out there. I have even seen domestic ADSL modem/routers on which the factory firmware contains such a client.
Doing it differently
But some time ago I got fed up with all the “account expiration warning” emails I had been getting from the service that I used. And since I’m quite familiar with the components you need for building such a service yourself, I set out to do so. The thing I ended up with is for personal use, specific to my needs, KISS, and sinful.
Sins!
It allows for instant creation/updating of DNS records through a very simple HTTP GET. That is sinful for two reasons:
- No authentication and authorization.
Anyone can create any record. And anyone can update any record he or she knows the name of. You understand how this could be a bad thing. Don’t have such names as MX records unless you don’t mind that mail intended for you ends up on someone else’s server.
At the same time it is a good thing, as I will show in the use case example below. - The HTTP GET alters state.
And that’s just plain Bad Taste. If you’re designing a web service you’ll usually try to make it RESTful. GET should be a safe method. But for this application, at the frontend, it is actually desirable — because grandma can GET, as you will see in the use case below.
Use cases
-
Case A:
Imagine you’re on the phone with your grandma, and after discussing apple pie recipes something else comes up and you need to know her IP (let’s say you need to SSH in and help her with her crontab). She hasn’t registered with any dynamic DNS service, she has a hard time determining her IP, she has a hard time reading it out loud without making mistakes, and you are fed up with typing IPs anyway. But she has a web browser. Wouldn’t it be nice if you could just tell her to open her browser and head over to http://grandma.reg-a-record.tld/reg.php ? Nothing to install, nothing to look up, and something reasonably simple for grandma to do. The script on http://grandma.reg-a-record.tld/reg.php creates a DNS A record named ‘grandma’ on dyndomain.tld, and this record points to your grandma’s IP. Now you can just do ’ssh grandma.dyndomain.tld’.
-
Case B:
Imagine you and your friends are on a coding binge, all running their Django/Rails/Node.js/Thing-du-jour instances on laptops, in the same room, and you all want to connect to each other’s development servers.
You could all call out your IPs across the room and let everyone type in everyone else’s IPs. Now you all have N tabs open in your browsers. Who was 123.231.101.45 again?
Or, everyone could just point their browser (or curl, wget…) to http://theirfirstname.reg-a-record.tld/reg.php2 and be done with it. If you want to view Fred’s progress you just go to fred.dyndomain.tld.
So, sin #1 & #2 allow for use cases that were not possible with the provider I was using.
You don’t have to register the records up front with some service provider. You can make up records as you go, and you want to be able to let anyone create any record. Hence sin #1. But it’s a good thing. You don’t have to reuse records, which is better for your privacy. With the commercial service I could only use a couple of names, so anyone whom I once had told to go to kleinebeer.ath.cx could kinda virtually follow me around and determine whether I was at work, home, uni or girlfriend by resolving this record. When creating new records is incredibly easy, there’s no reason to keep using the same name every time.
On to the code
It’s very, very simple! You need very little code. Things can be made much simpler than I have done. For byzantine reasons I use two webservers, one has a python CGI which creates the actual records, and the other one runs the public-facing PHP script (through mod_php) which you actually connect to.
DNS server
You need a domain for which you run the authoritative name server. I’m using the SheerDNS DNS server package on mine, for no good reason. Here’s a patch I made so that sheerdnshash creates directories with some group permission bits set, something I need in my setup.
You can use other DNS servers, of course. I discovered that the Unbound DNS server has a socket protocol which you can possibly use to add records on the fly. But I use sheerdns now, and here’s a Python module I made to write A-records in sheerdns’s directory structure. Public domain, use it as you see fit. You need to modify the constants to reflect your config, and you can test it by running the module from a shell.
#!/bin/env python import socket,os,string,subprocess,sys SHEERDNS_DIR="/var/sheerdns" SHEERDNS_HASH="/usr/sbin/sheerdnshash" DYNDOMAIN="dyndomain.tld" ALLOWCHARS=string.ascii_letters + ''.join([ "%d" % i for i in range(10)]) + '-' def mk_A(name,ip): #are host and ip allright? host = ''.join([ c for c in name if c in ALLOWCHARS ]).lower() if not host: return None try: socket.inet_aton(ip) except socket.error: return None fqdn = "%s.%s" % (host, DYNDOMAIN) #set the umask oldumask = os.umask(0002) #get the hash hash = subprocess.Popen([SHEERDNS_HASH, fqdn], stdout=subprocess.PIPE).communicate()[0].strip() #construct the dirpath path = os.path.join(SHEERDNS_DIR,hash,fqdn) #chmod it for everyone to read os.chmod(path,0755) #construct the A-record-filepath apath = os.path.join(path,'A') #open the record file with open(apath,'w') as record: record.write(ip) os.umask(oldumask) return name if __name__ == '__main__': if len(sys.argv) == 3: print(mk_A(sys.argv[1],sys.argv[2]))
Web ’service’ to create the records: a CGI
This is a Python CGI which uses the above module. I stick it behind HTTP Basic authentication which is handled by the web server. You call it like this: http://server_that_has/the_CGI?name=therecordname&ip=theip .
There’s no reason that this CGI should be guilty of Sin #2, except laziness on my behalf. If I’d be following every convention even for silly little solo projects I’d never get anything done, all right?
It doesn’t propagate any errors from the sheerdns library, but it prints the name as it is registered — stripped of disallowed characters, or nothing if something failed along the way (invalid IP, for instance).
#!/usr/bin/python import sheerdns import cgi print "Content-type: text/plain\n\n"; params = cgi.parse() name, ip = params.get('name'), params.get('ip') if ip and name: dnsname = sheerdns.mk_A(name[0],ip[0]) print dnsname
The accessible part: reg.php
This is in PHP. It lives in the default virtual host of my Apache config, and for good reasons. I have a CNAME record that points *.reg-a-record.tld to this web server. I then use whatever is in * to construct the name for the dyndomain.tld-record. The wildcard record is essential.
You need to modify this script to reflect your config.
<?php header('Content-Type: text/plain'); $hostparts = explode('.', $_SERVER['SERVER_NAME'], 2); $ip = $_SERVER['REMOTE_ADDR']; print $ip; $cha = curl_init('https://server_that_has/the_CGI?name='.$hostparts[0].'&ip='.$ip); curl_setopt($cha, CURLOPT_SSL_VERIFYPEER, False); curl_setopt($cha, CURLOPT_TIMEOUT, 5); curl_setopt($cha, CURLOPT_RETURNTRANSFER, True); curl_setopt($cha, CURLOPT_USERPWD, 'bite:me'); $resp = curl_exec($cha); curl_close($cha); print($resp); ?>
Presto
If you visit http://kitten.reg-a-record.tld/reg.php, the IP address that the web server sees you coming from gets registered as kitten.dyndomain.tld. If everything went well, the script responds with this mapping.
You could set DirectoryIndex reg.php here, or rename reg.pgp to index.php, if you don’t want users to type ‘reg.php’. I did make it explicit, because web sites on this server come and go and I don’t want people to stumble onto reg.php every time they hit the default vhost because their web site doesn’t exist any more.
1)Let’s assume you’re not NATed.
2)Try to only have friends with distinct names that can be expressed in ASCII.
Tags: code, dns, dyndns, English —

