Welcome to Arkanis Development

Build your own DynDNS

Published

During the last few weeks I wrote MiniDynDNS to build my own dynamic DNS service. Something like DynDNS but all by myself. This post explains the basic steps needed to wire MiniDynDNS into the worldwide DNS system.

I'm using it to create DNS names that point to devices at home I want to access via the internet. This is pretty nice with IPv6 since every device gets its own public IPv6 address. But please make sure only the services you want to have available are actually listening on the public IPv6 address. Or configure your firewalls accordingly.

To build your own DynDNS you'll need a few bits and pieces:

  • A server with a static IP address. Here we'll use 203.0.113.17 as a placeholder for that IP.
  • A registered domain of your own. example.com shall proudly serve as a placeholder for that domain.
  • Access to the nameserver or DNS records of that domain. We'll need to add some DNS records there.

The bigger picture

The whole idea of this operation is to create a subdomain that is managed by a program running on your server. Here we'll use dyn.example.com but it can be anything as long as it's a subdomain of your registered domain. Whenever someone on the world resolves a name like weather.dyn.example.com they're going to ask that program on your server to get the current IP of that name.

For that we first need a program running on your server that can answer DNS requests and allows us to update these IPs when they change. Obviously we're going to use MiniDynDNS for that. That's why I wrote it.

Second we need to tell the global DNS system that the program running on your server is responsible ("authoritative") for dyn.example.com subdomain. This is called "delegating" a subdomain. When you bought your own domain you also bought the right to delegate subdomains to whoever you deem worthy. With that in place whenever someone resolves a name in dyn.example.com they'll ask MiniDynDNS on your server.

Note that you can only delegate a subdomain to a host, e.g. ns.example.com. This host then has to resolve to 203.0.113.17. You can delegate to whatever host you want but in the end this host has to resolve to your public IP. Here we'll use ns.example.com as a placeholder for that.

The final part is a script running on whatever device or computer you want to have a dynamic domain name for. That script will periodically report its current IP to your MiniDynDNS.

If everything works correctly you can add any devices you want to your dyn.example.com subdomain and access them from everywhere on the world. pi.dyn.example.com, weather-station.dyn.example.com, tv.dyn.example.com, touchtable.dyn.example.com, spidercam.dyn.example.com or whatever. Get creative.

So lets get to it.

1. Run MiniDynDNS on your server

Download or clone MiniDynDNS from GitHub and do the "installation". Basically that's renaming config.example.yml to config.yml and setting the proper values for your setup. The domain, soanameserver and soamail are the important ones.

For MiniDynDNS to answer incoming DNS requests it has to listen on port 53. That's where other servers or clients expect to get DNS requests answered. Changing that will probably break things.

Per default it will use port 80 for a simple HTTP API with which we can update DNS records. In case this port is already used by a webserver like Apache you can change it to something else like 8080. We only need it for the scripts that periodically report the IPs of your devices to the server.

You can tell your server system to start MiniDynDNS on server startup. For me it's just a funny hobby so I leave it running in a screen terminal. You might also need to tell your servers firewall to open port 53 and 80 for incoming traffic (or whatever port you use for the HTTP interface). Otherwise the firewall will block everything and you'll just get timeouts.

Now your basic DynDNS server should already be up and running. To test it you can fire up a terminal and try this command:

nslookup foo.dyn.example.com 203.0.113.17

This tells nslookup to resolve foo.dyn.example.com by asking the DNS server 203.0.113.17. If everything works well it should tell you that foo.dyn.example.com has the IP address 192.168.0.1.

This assumes that you use the default database (just renamed db.example.yml to db.yml). If you already changed your DB you have to change the domain name in the command accordingly.

2. Delegate dyn.example.com to the MiniDynDNS server

Now on to tell the rest of the world that your server manages the dyn.example.com by itself. For this you have to add two records to your normal nameserver. In my case the company where I registered my domain provides a webinterface for this task.

You have to add these two DNS records to your example.com nameserver:

dyn  NS  ns.example.com
ns   A   203.0.113.17

The first record tells the DNS system that the dyn.example.com subdomain is delegated to ns.example.com. The second record says that ns.example.com has the IPv4 address 203.0.113.17. Please remember to replace the domain name and the IP with your own values. If your server also has an IPv6 address you should add an AAAA record for that, too.

3. Update your IPs periodically

This differs greatly between IPv4 and IPv6.

With IPv4 only your router has a public IP address. For every service you have to create a port forwarding to the appropriate internal computer or device. So in any way there's only one public IP to report to the DNS server. Many routers already have build-in support for this. Usually it's hidden somewhere in the webinterface called "dynamic DNS", "DynDNS" or something like that.

In the case of my FritzBox router I have to enter the domain (foo.dyn.example.com), a user name (just foo), the password (bar) and an update URL (http://ns.example.com/?myip=<ipaddr>). The router will replace "<ipaddr>" with its current public IPv4 address. It seems like it then just fires this HTTP request every 30 minutes. Again these values are based on the default db.yml file.

The required steps are probably quite different for other routers. You might even have to look into the manual or search a bit to figure out how to do this.

With IPv6 the situation is a bit simpler. Each device gets its own public IPv6 address. A script that runs every few minutes can simply report that IP to the DNS server. In case of a RaspberryPi running Raspbian this script will do the job:

IP=$( \
    ip addr show dev eth0 scope global | \
    grep --perl-regexp --only-matching '(?<=inet6 )2003:[0-9a-f:]+' | \
    head --lines 1 \
)
curl -s http://foo:bar@ns.example.com/?myip=$IP

Again, "foo", "bar" and "ns.example.com" are values from the default db.yml. Replace them with the values for your setup. In case you changed the port of the webinterface you also have to add a port to the HTTP request (something like http://foo:bar@ns.example.com:8080/?myip=$IP for port 8080).

Save it to /home/pi/update_ip.sh and make it executable with chmod u+x update_ip.sh. When you run it (./update-ip.sh) you should see something like "Your IP has been updated".

To execute the script every 5 minutes you just need to add a line to your crontab. The command crontab -e should show you an text editor and by adding this line at the end you should be set:

*/5  *  *  *  *  /home/pi/update_ip.sh 2>&1 >> /home/pi/update_ip.log

The "*/5" at the beginning means "every 5 minutes". If you want to run the script every 30 minutes its "*/30".

Done!

Phew, this was more text than I expected. When you run the command nslookup foo.dyn.example.com you should now see 192.168.0.1 as a result (again, default database values). Note that this command asks the nameserver provided by your environment (e.g. by your ISP). Thanks to the domain delegation this DNS request should end up at your nameserver which can then answer it. When you have a webserver running on one of your devices you can even use these domain names to access websites.

Anyway, with that setup you should be able to manage your own subdomains. The MiniDynDNS readme has some more information and useful commands so better take a look there, too.

Have fun with MiniDynDNS. :)

20 comments for this post

leave a new one

#1 by
Weeber
,

I haven't implemented this yet but it looks like it's going to be very useful. Thanks!

#2 by
heru.f
,

2016-09-20 09:19:08 SERVER: Running DNS on 0.0.0.0:53, HTTP on 0.0.0.0:80, as user nobody:nogroup 2016-09-20 09:19:18 HTTP: GET /?myip=192.168.5.10 -> not authorized 2016-09-20 09:19:18 HTTP: Failed to process request: Permission denied @ rb_sysopen - db.yml dns.rb:86:in `write' dns.rb:86:in `save_db' dns.rb:457:in `block in handle_http_connection' dns.rb:427:in `catch' dns.rb:427:in `handle_http_connection' dns.rb:582:in `block in <main>' dns.rb:568:in `loop'

how to resolve this…

#3 by
Stephan
,

Looks like the server can't access the db.ymlfile. Make sure that it exists and that the server has access to it. Per default the server loads the db.yml file from the current working directory. You can specify the path with the --db command line option.

Hope that helps.

#4 by
windows10times
,

That's fabulous. Came across various issues till date with the DNS and yup they are no more. Cool Post.

#5 by
Chris
,

Seems that minidyndns will not resolve itself:

DNS: A dyn.example.com -> wrong domain, ignoring

How to set an A record for "@"?

Thanks, Chris

#6 by
Stephan
,

Hi Chris,

never thought about that use case actually. The server had a check in place that it only resolves subdomains and nothing else. I loosened that check a bit and added the "@" name to represent the server itself. I pushed it online as version 1.1.1.

To define an IP address for the server itself you just have to add a record for "@" in the YAML database. Something like that:

"@":
  pass:
  A: 192.168.0.4
  AAAA: ff80::4

Since it's YAML you have to quote the @ sign. It's a normal record so you can even change it via the HTTP/HTTPS interface by using "@" as a username. I'm not sure this is a good idea so it's probably best to disable that by setting an empty password (makes the record unchangable via the web interface).

I hope this does what you need. :)

ps.: You just have to download the new dns.rb file. Nothing else changed.

#7 by
Fernando
,

Hi there, thanks a lot for writting this guide. Have you ever though on writting this but for Synlogy DSM OS?

This would be something great to have running on my NAS :)

Thanks again

Fernando

#8 by
Stephan
,

Hi Fernando,

thanks. Hm, never heard of Synlogy DSM OS but it looks quite nice. Maybe their SDK supports Ruby. In that case you could create a package for it. Unfortunately I don't have a NAS so I can't help there. :(

#9 by
SebiTNT
,

Hi there, at first: Thank you for this great tool and howto! I have a few questions and a little fix to share. I ran into some difficulties adding my letsencrypt certificates to use with this dyndns server. The problem was, that the letsencrypt script queried my servername "dyn.example.com" with some strange uppercase/lowercase mix, while it was creating my certificates. After some searching I found out, that this behaviour is called "0x20 Bit encoding" and is for some safety purposes. Because the server was configured to reply only to the lowercase version, the certificate creation was aborted, because the server didn't reply to DyN.exAMPlE.com in example. Thus lead me to adding .downcase in line "if domain.downcase == $config["domain"] and (type == TYPE_SOA or type == TYPE_ALL)" and in line "log "DNS: #{type_as_string} #{domain} -> wrong domain, ignoring" and return nil unless domain.downcase.end_with?($config["domain"])" These changes fixed it for me and my certificate was created correctly and is now in use with this dyndns-server. Because I'm not familiar with ruby and dns, please let me know, if this was a bad idea or not.

One question I have to ask is, if there is a chance of disabling the non-https-server for only allowing https connections. Is there any chance to do this or is the http essential for this to work?

Sebi

#10 by
Stephan
,

Hi SebiTNT,

thanks for the feedback and the fix! Especially the "0x20 Bit encoding" thing was very helpful. It made me realize that DNS names are actually case insensitive. Sounds stupid but I didn't realize that until now. Your fix doesn't just apply to the SOA record but to all records.

So I did a bit of reading and changed the server to case insensitive. Maybe that will fix some other elusive bugs, too (I'm looking at you, FritzBoxes). While I was at it the server also got the option to disable the HTTP interface. Just set the entire "http" key to "false" in your config. Same as with HTTPS. You can even disable both HTTP and HTTPS if you want.

So to answer your question: No the HTTP server isn't essential. I just didn't thought about adding a configuration option to disable it.

I tested and commited the stuff and put it online as release v1.1.2. Feel free to give it a try. :) You'll just have to download the new dns.rb file and you're done.

Thanks again for taking the time to look into this and sharing you fix. Comments like this make maintenance actually quite pleasent. And that's worth a lot to me. :)

#11 by
SebiTNT
,

Hi Stephan,

thank you very much for your kind reply and an instant implementation!

I've also seen another issue after sending a dns request to the server via my FritzBox. I get entries like "DNS: @ -> no records returned" in the log, especially if the request comes from my FritzBox. Note that there is a space character before the @. Other "normal" @-Queries are answered correctly. Do you know what this kind of query is for and how it can be reproduced without a FritzBox? Should there be a fix in your server tool?

Kind regards Sebi

#12 by
Stephan
,

Hi Sebi,

Now that's an interesting hint. And a strange log entry. Usually there should be a DNS record type before the name. Something like "A", "AAAA", "ANY" or "???". Since that part is missing I would guess that the server got a query for a DNS record type it doesn't understand. We better find out what record type the FritzBox asked for.

Hm, I looked into the code and seems like the "???" case didn't work. Not that is was a terribly useful. Fixed that and the server now logs unknown DNS record types by their decimal type (e.g. "5" for CNAME). Reason for that is that the server only prints types by name that it actually supports (e.g. "AAAA"). And I don't want to imply via the log messages that you can add CNAME or other unimplemented records to your db.yml file. Given the record type we also should be able to reproduce the query with tools like nslookup or dig.

Anyway, tested the changes and pushed that online as v1.1.3. Again you just have to download the new dns.rb file. After that the server will show something like the following for queries with unknown DNS record types:

DNS: type(5) foo -> no records returned

The server received a question for the DNS record type 5 (CNAME) of the subdomain "foo". Here's a nice list of the different record type and their decimal values: https://en.wikipedia.org/wiki/List_of_DNS_record_types

Let me know what record type gets requested. Then we can figure out if there's a new record type that needs implementation. :)

Happy programming Stephan

#13 by
SebiTNT
,

Hi Stephan,

thank you again so much!!! I have just tried it and it is asking for the NS record (decimal type 2). I've seen the same request from 194.150.168.168 and 213.73.91.35 (recommended dns-servers from the CCC website). So maybe an answer to this type of request should be added?

I really like your ddns-server because it is very compact and simple to understand!

Sebi

#14 by
Stephan
,

Hi Sebi,

If other servers ask MiniDynDNS for an NS record something about the setup is probably amiss. Except you actually want to create a nested hierarchy of dynamic DNS servers.

Usually the NS record should be provided by the nameserver of the company you rented your domain from. MiniDynDNS should only answer queries for the subdomain you delegated to it or subdomains of that subdomain, and so on.

For example when you rented the domain example.com from Domain Factory their DNS servers should provide the following records:

            A   <IP the webserver for example.com runs on>
www         A   <IP the webserver for www.example.com runs on>
minidyndns  A   203.0.113.17
dyn         NS  minidyndns.example.com

Note that you have to edit all those records in the webinterface of e.g. Domain Factory. Those records are under their authority and they operate the DNS servers that answer those queries. Your MiniDynDNS should never see any queries about those records.

The important record is the last one: "dyn NS minidyndns.example.com". This tells the world wide DNS system that the subdomain dyn.example.com is under the authority of the DNS server running on minidyndns.example.com. All queries regarding dyn.example.com or its subdomains should be directed there.

Now to ask minidyndns.example.com any questions we need to know its IP address. And that's what the "minidyndns A 203.0.113.17" is for. It tells anyone the IP they have to ask regarding questions about dyn.example.com and its subdomains. All this assumes of course that you run MiniDynDNS on a server with the IP 203.0.113.17. So change the IPs and names according to your setup. :)

To make it short: The NS record delegates the authority to minidyndns.example.com and the A record above tells us the IP of that lucky one. With that we can fire the UDP packet with the DNS query to the one that knows the answer.

Now on 203.0.113.17 there should run a MiniDynDNS that actually answers those queries. For example it can provide the following records:

    A  <IP the webserver for dyn.example.com runs on>
pi  A  <public IP of your router with port-forwardings to a Raspberry Pi>

The first record is for dyn.example.com and you would configure that with the "@" name in your db.yml. The second record is for pi.dyn.example.com.

Note that this in-between layer of dyn.example.com is necessary to establish the authority of your MiniDynDNS. With this kind of setup you can't for example host pi.example.com with your MiniDynDNS. Simply because subdomains of example.com are under the authority of company you rented your domain from. For those records you have to use the webinterface of that company.

Now there are 2 ways that might allow you to do it anyway:

1) Create pi.example.com as a CNAME record that redirects to pi.dyn.example.com. That way you can ask for pi.example.com but the DNS system will actually give you the IP of pi.dyn.example.com. You'll have to configure those records by hand via the webinterface of your domain provider. It would look like this: "pi CNAME pi.dyn.example.com". That's what I'm using right now.

2) You can take a look at DNAME records. Haven't used them yet but they might solve this. But I'm not sure about that.

That got a lot longer than expected. Sorry abut that. I hope the information helps. :)

Happy programming Stephan

#15 by
SebiTNT
,

Hi Stephan,

thank you for your long but very comprehensible answer!

The problem I see here is, that AFAIK dyn.example.com should know, which its responsible nameserver is (and ns.example.com [or minidyndns.example.com] should know the same). This is of course not essential for the basic functions, because normally it should go the other way around (com -> example -> dyn).

I did some tests on online nameserver test tools like http://dnscheck.ripe.net/ and they all said something like "No name servers found at child" and reported this as a fatal error. This is why I think a reply to a ns request should really be implemented, or do you have another opinion?

Have a nice Weekend! Sebi

#16 by
Stephan
,

Hi Sebi,

oh, I didn't know about that second function of NS records. Sorry about rumbling on about the basics before. :D

The server now answers NS queries about itself with a single NS record. It uses the same nameserver value as configured for the SOA record.

I could reproduce the "No name servers found at child" errors with RIPE DNSCheck. But even after implementing the NS answer it didn't work. The tool didn't even send a query to the server. Maybe the negative answer from before got cached so it might takes a day until I can check again with that tool.

On the other hand https://mxtoolbox.com/SuperTool.aspx did work quite well (even with some useful documentation!). At least my setup now passes the checks there. A warning about the SOA serial number format remains but I'm not really inclined to "fix" that. In the end it's just a number and gets incremented on every change.

I've pushed the changes online as version 1.1.4. Feel free to give it a try. :)

Happy programming Stephan

#17 by
Visitor
,

Hi, am i reading this right. We can set up our own Dyndns server to allow ourselves to notify ourselves that our dynamic IP has changed just like with NoIp and dyn DNS services?

Could this be done on a QNAP server using a virtual machine?

Regards Visitor

#18 by
Stephan
,

Not really. You need a server with a static IP to run MiniDynDNS on. Once you have that running devices like your QNAP can use that server like NoIp or others.

In effect you can setup your own dynamic IP service. But you need a proper server with a static IP for that. In the end the DNS needs an IP to ask for what a domain name means. And this IP has to be written down in a DNS record (see step 2 "Delegate dyn.example.com to the MiniDynDNS server" above).

In my case I have a vserver with a static IP address. So I use that to run MiniDynDNS on. Some of my home devices then use that MiniDynDNS to publish their IP addresses so I can access them.

Note that the server you run MiniDynDNS on doesn't have to be "in the internet" or in some server farm. As long as you have a machine with a static IP it'll work. In Germany a static IPv4 address will cost a bit so most people don't have one. In other countries this is different. So maybe you're lucky. Also with IPv6 you might get a static IP more easily or already have one. So you can use that static IPv6 address. But I haven't yet tried running MiniDynDNS via IPv6 only.

#19 by
Nixola
,

Hi! I'm trying to set up a dynamic DNS on a server using minidyndns, but I'm having issues with it. When I run nslookup I get a connection timeout and minidyndns prints

DNS: A foo.dyn.example.com -> wrong domain, ignoring

even though foo is set up in the database (note: not actually foo, and not actually example.com). Any idea why that might be?

Furthermore, I tried checking it with http://dnscheck.ripe.net/ and it says it can't find delegation or nameservers at parent, even though I set up the NS record as well…

#20 by
Stephan
,

Hi Nixola,

The "wrong domain" error occurs when the server receives a query for a domain that doesn't end in the value configured in the "domain" option. Maybe you forgot to restart the server after changing the configuration? Or maybe there is just a typo in the domain name (either in the query or in config.yml).

You can test this in isolation by sending a DNS query directly to your server. There are the dig and nslookup commands for a server running on 127.0.0.2 and configured for the domain dyn.foobar.com:

dig @127.0.0.2 abc.dyn.foobar.com A
nslookup abc.dyn.foobar.com 127.0.0.2

With those the server should reply with either an answer (if there is an IP for the name "abc") or "no records returned".

Regarding the RIPE DNS check: I'm not sure what happens there. My own setup also doesn't pass any tests there even though it works fine. The RIPE checker doesn't even send a packet to my server. I guess I'm still missing one last bit of magic in the domain delegation process but I can't figure out what that should be (I'm by no means an expert when it comes to DNS).

There are some other DNS checkers out there but most of them seem to be reskinned versions of the RIPE checker. The first genuine different tool that I stumbled upon (https://mxtoolbox.com/SuperTool.aspx) on the other hand shows all green (more or less). So I can only recommend to recheck your setup with that tool.

Happy programming Stephan

Leave a new comment

Having thoughts on your mind about this stuff here? Want to tell me and the rest of the world your opinion? Write and post it right here. Be sure to check out the format help (focus the large text field) and give the preview button a try.

Format help

Please us the following stuff to spice up your comment.

An empty line starts a new paragraph. ---- print "---- lines start/end code" ---- * List items start with a * or -

or