TinyDNS as a Puppet Module

As a data center operator we are in need of DNS handling for our internal servers and appliances. Handling IP’s for everything is not really an option and also changes to servers are much easier to handle if we can use assigned domain names instead of IP addresses. When we started looking at open source DNS servers for the Linux operating system there are really 2 bigger contestants out there in the market: Bind and TinyDNS. This said Bind pretty much owns the market. That comes with advantages and disadvantages.

One big advantage of Bind over TinyDNS is the amount of documentation and support on the community. It’s been around for so long you find pretty much any answer to any question you’ll ever have. The big downsides of Bind are its size and the fact that it is used all around. So obviously security vulnerabilities are mostly exploited on commonly used systems since you get more bang for your buck.

Taking everything into account we decided to go with TinyDNS as our preferred DNS server setup. One thing I found relatively quickly is the lack of serious and complete documentation on the operation and setup of a fully functional server. As usual in these cases you use several sources and fill in the blanks as much as possible. In this case it worked out nicely and I even was able to put all the configuration into a nice Puppet module for maximum reusability.

Some sources I relied on:

We use either CentOS or Ubuntu for our server setups here. Since TinyDNS is a supported package in Ubuntu but not CentOS I went with Ubuntu as the base OS in order to not additionally having to deal with compilation issues and for easier upgradability through the standard package management system.

So the TinyDNS module is available through Puppetlabs PuppetForge and the sources are avialble through GitHub.

So how do I use this module?

Include the TinyDNS class in your host manifest or in your host setup in Puppet Enterprise (PE) which is what we are using. All our modules are written with PE in mind which results in the ability to either use parameter configuration when calling the class from e.g. a site.pp or alternatively using a global variable assignment which can be assigned through the PE console.

Generally we use subclasses within our classes for easier distinction of what code is doing what. So we have a main class (tinydns), an install class (tinydns::install), a configuration class (tinydns::configure) and a service class (tinydns::service). In this case we also have an additional specific configuration class for our domain (tinydns::yourdomain_net) which holds all the specifics for the given domain. This may also be handled with a different system like hiera, at the moment we are using this easy and straightforward method that anybody can use out of the box.

The main class looks like this:

# Super class for TinyDNS - handles global and class variables
#
class tinydns (
  $tinydns_external_ip = "127.0.0.1",
  $dnscache_external_ip = $::ipaddress,
  $addl_server_ip = "none",
)

This allows us to specify 3 things:
The external IP for the TinyDNS server itself. In our case since we are not using this as a external DNS we keep it local and use the localhost to advertise the DNS records. Important to note here that TinyDNS and DNSCache both need to be listening on separate IP addresses. Since this module also configures the Cache it makes sense to have the TinyDNS server on localhost and the cache on the outbound IP.
The external IP for the DNSCache. This is in our case the IP of our internal DNS server which we’ll usually get from facter as a global variable. In case multiple IP’s are assigned to the server you’re using you might want to specify specifically which one to use here.
Additional servers to cache for. This allows for other DNS servers that are setup to cache for for DNSCache. Usually only necessary if you are running multiple DNS servers for sub segments of your network.

The tinydns:yourdomain_net should get renamed to whatever your real domain is called, e.g. copperfroghosting_net in our case. You can then modify the names of the classes inside. In this case the classes are:
tinydns::yourdomain_net::ns_setup - This defines the namespace configuration for your setup
tinydns::yourdomain_net::host_setup - This defines all host entries for your setup
tinydns::yourdomain_net::alias_setup - This defines all alias configurations you may have
Each of the classes has examples already predefined for easy reconfiguration and adaptability to your actual needs. The host_setup class has all IP’s from .1 - .199 predefined for easy configuration. This way you just need to uncomment an entry and add the actual data to it. Makes sure you don’t accidentally double set an IP and gives you a quick overview of what’’s taken and what’s not.

Adding DNS Entries

This is done in the above classes via the following define method:

# This method adds a DNS entry depending on it's given type.
# The $name is unique in the DNS list. If the name needs to change you first
# need to remove the entry and then add a new one.
define tinydns::add_dns_entry (
  $type,
  $ip,
  $expiry
)

The fields for the add_dns_entry are pretty self-explanatory but here for clarification:
name - This is the unique name of the DNS entry and is also used to identify entries in the DNS table.
type - Can be either one of the three:
host : host entry (main entry for an IP), e.g. server1.yourdomain.net
alias : alias entry (secondary entries for an IP), e.g. mail.yourdomain.net -> server1.yourdomain.net
ns : Namespace entry, e.g. 10.10.10.in-addr.arpa
ip - The IP address to point DNS entry to, e.g. 10.10.10.123
expiry : Expiry time of an entry in cache in seconds, e.g. 3600

That’s pretty much it. After you reconfigure your yourdomain_net class to reflect your actual setup, just add the class to your server and run the puppet agent.

If at any time later in the game you need to remove entries that can be achieved with the define method tinydns::rem_dns_entry. Just use the unique name to eliminate and entry from the table. If you need to change an entry make sure to delete the old entry first then add the new entry. There is a dependency issue when changing things in the same run that I still have to work on to fully resolve.

The power of Augeas

One thing I want to point out here is that I also wrote an Augeas lens to better control the data file for TinyDNS. This alone presented a very unique set of challenges, since I found that the documentation on that is about as thorough as the one on TinyDNS. Especially the Augeas main pages are not very helpful. But after a while I figured out a nice way to do it.
It allows you to search specifically for entries and change the data accordingly. The nice thing about augeas is, with the right lens, that you can search for a specific entry and if it’s not in the file yet you can add it at the same time.
In this case however we are only changing values in the file for existing entries or to remove entries altogether.
Let’s look at some code. First we need to specify some reg expressions to capture valid masks for what we are looking for:

  let eol = Util.eol
  let sep = Sep.colon
  let sto_to_com_def = store ( /[.=+]/ )
  let sto_to_com_name = store ( /[A-Za-z0-9_.-]+/ )
  ...

We can use predefined masks from Sep and Util for commonly used values. Alternatively we can define them from scratch, e.g. accepting ‘.’, ‘=’, or ‘+’ or any character of the alphabet, numbers and underscore, dash and ‘.’. The ‘+’ at the end of sto_to_com_name defines that multiple characters are valid while the first masks only allow one character each.
‘store’ defines that Augeas actually stores the value in the file, vs. just being a label of some sort.

 let alias_field (kw:string) (sto:lens) = [ label kw . sto ]

We can define functions that have parameters which will then be substituted. In this case Augeas uses a label whcih is represented by string with the variable name ‘kw’ and a value with the variable name ‘sto’. In this case ‘sto’ will be replace by one of the above defined store functions.

  let spec = [ label "spec"
               . alias_field "def" sto_to_com_def
               . alias_field "name" sto_to_com_name . sep
               . alias_field "ip" sto_to_com_ip . sep
               . (alias_field "class" sto_to_com_class . sep)?
               . alias_field "expiry" sto_to_com_expiry
               . eol ]

This is the actual magic here. This defines what a valid line in the file can look like. Here we define a label that we can lookup, ‘spec’. Next there is a definition, then a name, then an IP address, then either a class or nothing, then an expiry value and last the end-of-line
This results in a line that looks something like this in the file:

  =puppetmaster.copperfroghosting.net:10.10.20.199:7200

The following line will define that the file can contain multiple of those valid lines:

  let lns = ( spec )*

Lastly we define valid filters for files to automatically be loaded by Augeas.

  let filter = (incl "/var/lib/tinydns/root/data") .
               (excl "*.bak") .
               Util.stdexcl

This might help understanding some more about Augeas lens creation.

Well enjoy the module and let me know if you have any feedback.

Filed under: 

About the Author

Interested? Let's talk.