Managing Bulk DNS Zones with Perl

A History of Forward and Reverse DNS

When an Internet server receives an incoming connection from a client, it may take a precaution of verifying the identity of the client. Some protocols simply trust the client to provide proper credentials to their identification (like SMTP). More complex protocols use a key exchange or a shared secret to communicate. Other protocols rely on the process of verifying the hostname of the client.

To do this, a server would take the IP address of the client, and perform a reverse DNS lookup to get the PTR records. Then, it would request the A record of the hostname returned from the PTR query. If the hostname matched the IP address in both queries, then the host was considered to be trusted.

As a means of host authentication, it was a half-hearted process at best. It relied heavily on external DNS servers, and it could easily be circumvented if false records were returned to the DNS resolvers that sent the queries. As such, it was only suitable for public Internet services like anonymous FTP or Web servers.

Another problem with this process was that it meant you would need to have matching forward and reverse DNS entries for all of your client hosts. Without the records, your logon could be delayed, or it could be refused altogether.

This became a burdeon for ISPs with large dial-up pools, or organizations with large LANs. Dozens, if not hundreds of IP addresses suddenly needed to have two DNS entries. DNS zones that originally contained 20 host records grew to hundreds of host records.

For example, let's say we work in a large university environment. We have been given the 192.168.76.0/22 and 10.10.0.0/16 network blocks. Before any of these IP addresses are useful, we need to create forward and reverse DNS entries for them.

The network engineer writes a simple Perl script that will populate the zonefile with entries. The output from the script is then put in the domain "ips.university.edu".


$fmt="%s\tIN\tA%s\n";
foreach $block ("192.168.76.","192.168.77.","192.168.78.","192.168.79.")
{
	$host=$block;
	$host=~s/\./-/g;
	for ($n=0;$n<=255;$n++)
	{
		printf($fmt,$host.$n,$block.$n);
	}
}

As a result of a script such as this, you'd find out that you have a host record for your IP that looks like this.


192-168-76-93.ips.university.edu

It sure isn't pretty, but it gets the job done.

As far as the DNS server is concerned, there is nothing wrong with this record. Some experienced network engineers will probably point out a couple of causes for concern.

1. The record size is big. For each A record, we're using anywhere from eight to 15 characters to represent a 32-bit integer.

2. The repeated "192-168-" pattern is wasteful. We could remove it from the entry, so 76-54.ips.university.edu points to 192.168.76.54, but that leads to a conflict when we create a DNS entry for 10.10.76.54?

We can correct that by creating seperate subdomains for both networks.


76-54.n1.ips.university.edu.	IN	A	192.168.76.54
76-54.n2.ips.university.edu.	IN	A	10.10.76.54

Using separate subdomains, we've reduced the size of the record. The XXX-XXX.nX portion can be anywhere from six to 10 characters. But we can still do a little more work to make it smaller.

The solution is to create a script that more accurately looks at the network block, and creates DNS entries based only on the host portion of the IP address in a block.

IP addresses and Net::Netmask

IPv4 addresses are 32-bit integers. The IP address 192.168.76.55 can be represented in several ways:

Dottted Quad Notation 192.168.76.55
Base 10 integer 3232255031
Hexadecimal c0a84c37
Binary 11000000101010000100110000110111

For future mathematicians out there, dotted quad notation is really base-256 notation.

Network masks are also 32-bit integers. The 192.168.76.0/22 network number represents the following netmask:


11111111111111111111110000000000

For any IP address within this block, the first 22 bits represent the network, the remaining 10 bits represent the host within the network. If we were to split the network and host values of the IP address 192.168.76.55, then we find:


Network Host
Host IP 1100000010101000010011 0000110111
Netmask 1111111111111111111111 0000000000
Base 10 3156499 55
Base 16 302a13 37

For the IP address 192.168.76.5, the host portion of the IP address is five.


Network Host
Host IP 1100000010101000010011 1100000010101000010011
Netmask 1111111111111111111111 0000000000
Base 10 3156499 5
Base 16 c0a84c37 5

For the IP address 192.168.77.87, the host portion of the IP address is 343.


Network Host
Host IP 1100000010101000010011 0101010111
Netmask 1111111111111111111111 0000000000
Base 10 3156499 343
Base 16 c0a84c37 157

In order to get the host information about our address block, we will be using the Net::Netmask Perl module.

The Net::Netmask module was written to disseminate information about network blocks. By giving it an IP address and a netmask, it can tell you the network address, the broadcast address, of a given network. It can also tell us which IP address corresponds to which host number.


use Net::Netmask;

$block=Net::Netmask->new("192.168.76.0/22");

print("Network can hold ",$block->size()," IP addresses\n");
print("Host 5   is IP ",$block->nth(5),"\n");
print("Host 343 is IP ",$block->nth(343),"\n");

The output of the program is:


Network can hold 1024 IP addresses
Host 5   is IP 192.168.76.5
Host 343 is IP 192.168.77.87

The nth() method returns the IP address corresponding to the host number.

The Updated Script

This new script accomplishes the same thing as the old script, but this time we use Net::Netmask to keep track of IPs in the block.


use Net::Netmask;

$network="192.168.76.0/22";
$fmt="%s\tIN\tA\t%s\n";

$block=Net::Netmask->new($network);
$size=$block->size()-2;
$index=1;
while ($index <= $size)
{
	$host=sprintf("h%x",$index);
	printf($fmt,$host,$block->nth($index));
}

Which produces the following zonefile.


h1      IN      A       192.168.76.1
h2      IN      A       192.168.76.2
h3      IN      A       192.168.76.3
h4      IN      A       192.168.76.4
....
h35     IN      A       192.168.76.53
h36     IN      A       192.168.76.54
h37     IN      A       192.168.76.55
h38     IN      A       192.168.76.56
h39     IN      A       192.168.76.57
h3a     IN      A       192.168.76.58
....
h3fb    IN      A       192.168.79.251
h3fc    IN      A       192.168.79.252
h3fd    IN      A       192.168.79.253
h3fe    IN      A       192.168.79.254

A couple of points to make.

1. In the host record itself, we choose to represent $index in hexadecimal. This gives us a little more use out of the ASCII character set. We save one character on integers between 99 and 255.

2. A prefix of "h" (for host) is used on every record, so we don't have records that are just an integer. For example, a record like "1.n1.ips.university.edu" can be a problem depending on your resolver search settings. If you try to run "ping 1" from a command prompt, then it's unclear whether you meant the host record "1," or the actual IP address "0.0.0.1".

3. The above script doesn't produce records for the network address or the broadcast address. It is left for the DNS administrator to create them or just ignore them.

Using the example 192.168.76.0/22 network, the zonefile created by the second script is roughly 70 percent of the size of the zonefile created by the first script.

Creating the Reverse DNS

We can use Net::Netmask to create the reverse DNS as well, but the process is a little tricker.

Reverse DNS zones are delegated on the octet boundaries of the IP address, so it usually means that a zonefile will cover a full /24 network. If your network is larger than a /24, then you'll need to create multiple zonefiles. If your network is smaller, then your data will probably be inserted to a zonefile that already exists for the other hosts in the network.

Note: While it is possible for DNS reverse zones to be delegated for networks smaller than a /24, the discussion of setting up the CNAME or NS records for those situations is outside the scope of this article.

This makes the creation of reverse DNS a little tricky. When you created the forward DNS, all of your data ended up in one zonefile. For the reverse DNS, the data could be in one or more zonefiles depending on the size of the network.

The Net::Netmask module has a feature that makes it a little easier to work this out. The inaddr() function returns data on the reverse DNS zones that the current netblock would occupy. For each /24 block your network covers, it returns the reverse DNS zone name, the starting IP address, and the ending IP address of the block.

So if we were looking at the 192.178.76.0/22 network, the inaddr() function would return:


76.168.192.in-addr.arpa
0
255
77.168.192.in-addr.arpa.
0
255
78.178.192.in-addr.arpa
0
255
79.178.192.in-addr.arpa
0
255

However, if we were looking at the 192.168.76.0/26 network, the inaddr() function would return:


76.178.192.in-addr.arpa
0
63

Using this function, we can create a baseline program that at least tells us which zones we would need to create reverse DNS for.


$block=Net::Netmask->new("192.168.76.0/22");

(@data)=$block->inaddr();
while (($zone,$start,$end)=splice(@data,0,3))
{
	print("; Reverse zone: $zone\n");
	for ($loop=$start;$loop<=$end;$loop++)
	{
		# Create the individual entries
	}
}

The outside loop runs once for every possible /24 network we will be filling with PTR records. The inner loop begins an iteration from the starting IP address and the ending IP address.

If you remember, the nth() method of Net::Netmask returns an IP address based on a host number. The match() method is the exact opposite; it returns a host number based on an IP address.


use Net::Netmask;

$block=Net::Netmask->new("192.168.76.0/22");

print("IP 192.168.76.5  is Host ",$block->match("192.168.76.5"),"\n");
print("IP 192.168.77.87 is Host ",$block->match("192.168.77.87"),"\n");

IP 192.168.76.5  is Host 5
IP 192.168.77.87 is Host 343

In order to get the host number, we would need to provide the match() function with a full IP address. This is the part where we have to do a little string cheating.


$block=Net::Netmask->new("192.168.76.0/22");

sub flip
{
my ($zone)=shift;
my ($network,@rz,@ipc);
(@rz)=split(/\./,$zone);
(@ipc)=reverse(splice(@rz,0,3));
$network=join(".",@ipc);
return $network;
}

$domain=".n1.ips.university.edu.";
$fmt="%s\tIN\tPTR\t%s\n";
(@data)=$block->inaddr();

while (($zone,$start,$end)=splice(@data,0,3))
{
        print("; Reverse zone: $zone\n");
	$network=flip($zone);
        for ($loop=$start;$loop<=$end;$loop++)
        {
		# Create the invidivual entries
		$ip="$network.$loop";
		$order=$block->match($ip);
		$host=sprintf("h%x%s",$order,$domain);
		printf($fmt,$loop,$host);
        }
}

The confusing part is probably the flip() subroutine. All that does is take a reverse DNS zone name (like 76.168.192.in-addr.arpa), and returns the quads in forward form (192.168.76). We use this string as a prefix to combine with $loop so we have a valid IP address for the match() method.

The program run gives us:


;Reverse Zone: 76.168.192.in-addr.arpa

1       IN      PTR     h1.n1.ips.university.edu.
2       IN      PTR     h2.n1.ips.university.edu.
3       IN      PTR     h3.n1.ips.university.edu.
4       IN      PTR     h4.n1.ips.university.edu.
5       IN      PTR     h5.n1.ips.university.edu.
6       IN      PTR     h6.n1.ips.university.edu
....
53      IN      PTR     h35.n1.ips.university.edu.
54      IN      PTR     h36.n1.ips.university.edu.
55      IN      PTR     h37.n1.ips.university.edu.
56      IN      PTR     h38.n1.ips.university.edu.
57      IN      PTR     h39.n1.ips.university.edu.
58      IN      PTR     h3a.n1.ips.university.edu.
;Reverse Zone: 79.168.192.in-addr.arpa

0       IN      PTR     h300.n1.ips.university.edu.
1       IN      PTR     h301.n1.ips.university.edu.
2       IN      PTR     h302.n1.ips.university.edu.
3       IN      PTR     h303.n1.ips.university.edu.
4       IN      PTR     h304.n1.ips.university.edu.

Other Tricks

We used hexadecimal numbers in the host records to save space in the zonefile, and to make the record a little more obscure to the casual observer. To make it a even more obscure, try using a subroutine that will create base32 numbers (0-9,a-v), or base36 (0-9,a-z) numbers.

To make it easier to identify the size of the network, create two A records for the zone itself, and populate it with the starting IP address, and the ending IP address.


;
; Forward DNS for 192.168.96.0/21 network
$ORIGIN n3.ips.university.edu.

	IN	NS	ns1.university.edu.
	IN	NS	ns2.university.edu.

	IN	A	192.168.96.0
	IN	A	192.168.103.255

h1	IN	A	192.168.96.1
h2	IN	A	192.168.96.2

Net::Netmask has two functions, base() and broadcast(), which can be used to obtain these values.

Conclusion

Your DNS records need to match in order to satisfy forward/reverse host authentication. It doesn't matter what the values are, just as long as they agree. It seems like a large hassel, especially when you consider that the practice of forward/reverse host authentication is considered highly untrustworthy by security administrators. Some hostmasters would say that the process is a waste of time. Keep in mind that there will always be one or two users that will demand that their desktop system be given forward and reverse DNS entries.

It's easy to automate the process of creating the zonefiles. Once the data is put in place, it almost never needs to be updated. I've seen a lot of DNS entries out there that simply replicate the IP address. Check the logs of a popular Web server and see for yourself.

Unfortunately, it's unlikely that these records will change in the future, unless the network allocation actually changes to another entity. In the meantime, the code examples above can be used to easily create new zones for future network allocations.

Visit the home of the Perl programming language: Perl.org

Sponsored by

Monthly Archives

Powered by Movable Type 5.13-en