5.3. Domain Name Service (DNS)
As useful as they are, NIS and WINS still suffer from flaws that make
them unsuitable for "entire-Internet" uses.
-
Scale
-
Even though these schemes allow for multiple servers, each server
must have a complete copy of the entire network topology.[4] This topology must be duplicated to every
other server, a time-consuming process if the universe becomes
sufficiently large. WINS also suffers because of its dynamic
registration model. A sufficient number of WINS clients could melt
down any set of Internet-wide WINS servers with registration
requests.
-
Administrative control
-
We've been talking about strictly technical issues up until
now, but that's not the only side of administration. NIS, in
particular, requires a single point of administration. Whomever
controls the master server controls the entire NIS domain lead by
that machine. Any changes to the network namespace must pass through
that administrative gatekeeper. This doesn't work for a
namespace the size of the Internet.
A new model called Domain
Name Service (DNS) was invented to deal with the flaws inherent in
maintaining host files or NIS/NIS+/WINS-like systems. Under DNS, the
network namespace is partitioned into a set of somewhat arbitrary
"top-level domains." Each top-level domain can then be
subdivided into smaller domains, each of those partitioned, and so
on. At each dividing point it is possible to designate a different
party to retain authoritative control over that portion of the
namespace. This handles our administrative control concern.
Network clients that reside in the individual parts of this hierarchy
consult the name server closest to them in the hierarchy. If the
information the client is looking for can be found on that local
server, it is returned to the client. On most networks, the majority
of name-to-IP address queries are for machines on that network, so
the local servers handle most of the local traffic. This satisfies
the scale problem. Multiple DNS servers (also known as secondary or
slave servers) can be set up for redundancy and load-balancing
purposes.
If a DNS server is asked about a part of the namespace that it does
not control or know about, it can either instruct the client to look
elsewhere (usually higher up in the tree) or fetch the required
information on behalf of the client by contacting other DNS servers.
In this scheme, no single server needs to know the entire network
topology, most queries are handled locally, local administrators
retain local control, and everybody is happy. DNS offers such an
advantage compared to other systems that most other systems like NIS
and WINS offer a way to integrate DNS. For instance, SunOS NIS
servers can be instructed to perform a DNS query if a client asks
them for a host they do not know. The results of this query are
returned as a standard NIS query reply so the client has no knowledge
that any magic has been performed on its behalf. Microsoft DNS
servers have similar functionality: if a client asks a Microsoft DNS
server for the address of a local machine that it does not know
about, the server can be configured to consult a WINS server on the
client's behalf.
5.3.1. Generating DNS Configuration Files
Production of DNS
configuration files follows the same procedure that we've been
using to generate host and NIS source files, namely:
-
Store data in a separate database (the same database can and probably
should be the source for all of the files we've been
discussing).
-
Convert data to the output format of our choice, checking for errors
as we go.
-
Use RCS (or an equivalent source control system) to store old
revisions of files.
For DNS, we have to expand the second step because the conversion
process is more complicated. As we launch into these complications,
you may find it handy to have the DNS and BIND
book by Paul Albitz and Cricket Liu (O'Reilly) on hand for
information on the DNS configuration files we'll be creating.
5.3.1.1. Creating the administrative header
DNS configuration files begin with an
administrative header that provides information about the server and
the data it is serving. The most important part of this header is the
Start of Authority (SOA) resource record. The SOA
contains:
-
The name of the administrative domain served by this DNS server
-
The name of the primary DNS server for that domain
-
Contact info for the DNS administrator(s)
-
The serial number of the configuration file (more on this in a moment)
-
Refresh and retry values for secondary servers (i.e., when they
synchronize with the primary server)
-
Time To Live (TTL) settings for the data being served (i.e., how long
the information being provided can be safely cached)
Here's an example header:
@ IN SOA dns.oog.org. hostmaster.oog.org. (
1998052900 ; serial
10800 ; refresh
3600 ; retry
604800 ; expire
43200) ; TTL
@ IN NS dns.oog.org.
Most of this information is just tacked on the front of a DNS
configuration file verbatim each time it is generated. The one piece
we need to worry about is the serial number. Once every
X seconds (where X is
determined by the refresh value from above), secondary name servers
contact their primary servers looking for an update to their DNS
data. Modern DNS secondary servers (like BIND v8+ or Microsoft DNS)
can also be told by their master server to check for an update when
the master data has changed. In both cases, the secondary servers
query the primary server for its SOA record. If the SOA record
contains a serial number higher than their current serial number, a
zone transfer is initiated (that is, the secondary downloads a new
data set). As a result, it is important to increment this number each
time a new DNS configuration file is created. Many DNS problems are
caused by failures to update the serial
number.
There are at least two ways to make sure the serial number is always
incremented:
-
Read the previous configuration file and increment the value found
there.
-
Compute a new value based on an external number source
"guaranteed" to increment over time (like the system
clock or RCS version number of the file).
Here's some example code that uses a combination of these two
methods to generate a valid header for a DNS zone file. It creates a
serial number formatted as recommended in Albitz and Liu's book
(YYYYMMDDXX where Y=year, M=month, D=day, and XX=a two-digit counter
to allow for more than one change per day):
# get today's date in the form of YYYYMMDD
@localtime = localtime;
$today = sprintf("%04d%02d%02d",$localtime[5]+1900,
$localtime[4]+1,
$localtime[3]);
# get username on either NT/2000 or Unix
$user = ($^O eq "MSWin32")? $ENV{USERNAME} :
(getpwuid($<))[6]." (".(getpwuid($<))[0].")";
sub GenerateHeader{
my($header);
# open old file if possible and read in serial number
# assumes the format of the old file
if (open (OLDZONE,$target)){
while (<OLDZONE>) {
next unless (/(\d{8}).*serial/);
$oldserial = $1;
last;
}
close (OLDZONE);
}
else {
$oldserial = "000000"; # otherwise, start with a 0 number
}
# if old serial number was for today, increment last 2 digits, else
# start a new number for today
$olddate = substr($oldserial,0,6);
$count = (($olddate == $today) ? substr($oldserial,6,2)+1 : 0);
$serial = sprintf("%6d%02d",$today,$count);
# begin the header
$header .= "; dns zone file - GENERATED BY $0\n";
$header .= "; DO NOT EDIT BY HAND!\n;\n";
$header .= "; Converted by $user on ".scalar((localtime))."\n;\n";
# count the number of entries in each department and then report
foreach my $entry (keys %entries){
$depts{$entries{$entry}->{department}}++;
}
foreach my $dept (keys %depts) {
$header .= "; number of hosts in the $dept department:
$depts{$dept}.\n";
}
$header .= "; total number of hosts: ".scalar(keys %entries)."\n;\n\n";
$header .= <<"EOH";
@ IN SOA dns.oog.org. hostmaster.oog.org. (
$serial ; serial
10800 ; refresh
3600 ; retry
604800 ; expire
43200) ; TTL
@ IN NS dns.oog.org.
EOH
return $header;
}
Our code attempts to read in the previous DNS configuration file to
determine the last serial number in use. This number then gets split
into date and counter fields. If the date we've read is the
same as the current date, we need to increment the counter. If not,
we create a serial number based on the current date with a counter
value of 00. Once we have our serial number, the
rest of the code concerns itself with writing out a pretty header in
the proper form.
5.3.1.2. Generating multiple configuration files
Now that we've covered the
process of writing a correct header for our DNS configuration files,
there is one more complication we need to address. A well-configured
DNS server has both forward (name-to-IP address) and reverse (IP
address-to-name) mapping information available for every domain, or
zone, it controls. This requires two configuration files per zone.
The best way to keep these synchronized is to create them both at the
same time.
This is the last file generation script we'll see in this
chapter, so let's put everything we've done so far
together. Our script will take a simple database file and generate
the necessary DNS zone configuration files.
To keep this script simple, I've made a few assumptions about
the data, the most important of which has to do with the topology of
the network and namespace. This script assumes that the network
consists of a single class C subnet with a single DNS zone. As a
result, we only create a single forward mapping file and its reverse
map sibling file. Adding code to handle multiple subnets and zones
(i.e., creating separate files for each) would be an easy addition.
Here's a quick walk-through:
-
Read in the database file into a hash of hashes, checking the data as
we go.
-
Generate a header.
-
Write out the forward mapping (name-to-IP address) file and check it
into RCS.
-
Write out the reverse mapping (IP address-to-name) file and check it
into RCS.
Here is the code and its output:
use Rcs;
$datafile = "./database"; # our host database
$outputfile = "zone.$$"; # our temporary output file
$target = "zone.db"; # our target output
$revtarget = "rev.db"; # out target output for the reverse mapping
$defzone = ".oog.org"; # the default zone being created
$recordsep = "-=-\n";
# get today's date in the form of YYYYMMDD
@localtime = localtime;
$today = sprintf("%04d%02d%02d",$localtime[5]+1900,
$localtime[4]+1,
$localtime[3]);
# get username on either NT/2000 or Unix
$user = ($^O eq "MSWin32")? $ENV{USERNAME} :
(getpwuid($<))[6]." (".(getpwuid($<))[0].")";
# read in the database file
open(DATA,$datafile) or die "Unable to open datafile:$!\n";
while (<DATA>) {
chomp; # remove record separator
# split into key1,value1
@record = split /:\s*|\n/m;
$record ={}; # create a reference to empty hash
%{$record} = @record; # populate that hash with @record
# check for bad hostname
if ($record->{name} =~ /[^-.a-zA-Z0-9]/) {
warn "!!!! ",$record->{name} .
" has illegal host name characters, skipping...\n";
next;
}
# check for bad aliases
if ($record->{aliases} =~ /[^-.a-zA-Z0-9\s]/) {
warn "!!!! " . $record->{name} .
" has illegal alias name characters, skipping...\n";
next;
}
# check for missing address
unless ($record->{address}) {
warn "!!!! " . $record->{name} .
" does not have an IP address, skipping...\n";
next;
}
# check for duplicate address
if (defined $addrs{$record->{address}}) {
warn "!!!! Duplicate IP addr:" . $record->{name}.
" & " . $addrs{$record->{address}} . ", skipping...\n";
next;
}
else {
$addrs{$record->{address}} = $record->{name};
}
$entries{$record->{name}} = $record; # add this to a hash of hashes
}
close(DATA);
$header = &GenerateHeader;
# create the forward mapping file
open(OUTPUT,"> $outputfile") or
die "Unable to write to $outputfile:$!\n";
print OUTPUT $header;
foreach my $entry (sort byaddress keys %entries) {
print OUTPUT
"; Owned by ",$entries{$_}->{owner}," (",
$entries{$entry}->{department},"): ",
$entries{$entry}->{building},"/",
$entries{$entry}->{room},"\n";
# print A record
printf OUTPUT "%-20s\tIN A %s\n",
$entries{$entry}->{name},$entries{$entry}->{address};
# print any CNAMES (aliases)
if (defined $entries{$entry}->{aliases}){
foreach my $alias (split(' ',$entries{$entry}->{aliases})) {
printf OUTPUT "%-20s\tIN CNAME %s\n",$alias,
$entries{$entry}->{name};
}
}
print OUTPUT "\n";
}
close(OUTPUT);
Rcs->bindir('/usr/local/bin');
my $rcsobj = Rcs->new;
$rcsobj->file($target);
$rcsobj->co('-l');
rename($outputfile,$target) or
die "Unable to rename $outputfile to $target:$!\n";
$rcsobj->ci("-u","-m"."Converted by $user on ".scalar(localtime));
# now create the reverse mapping file
open(OUTPUT,"> $outputfile") or
die "Unable to write to $outputfile:$!\n";
print OUTPUT $header;
foreach my $entry (sort byaddress keys %entries) {
print OUTPUT
"; Owned by ",$entries{$entry}->{owner}," (",
$entries{$entry}->{department},"): ",
$entries{$entry}->{building},"/",
$entries{$entry}->{room},"\n";
printf OUTPUT "%-3d\tIN PTR %s$defzone.\n\n",
(split/\./,$entries{$entry}->{address})[3],
$entries{$entry}->{name};
}
close(OUTPUT);
$rcsobj->file($revtarget);
$rcsobj->co('-l'); # assumes target has been checked out at least once
rename($outputfile,$revtarget) or
die "Unable to rename $outputfile to $revtarget:$!\n";
$rcsobj->ci("-u","-m"."Converted by $user on ".scalar(localtime));
sub GenerateHeader{
my($header);
if (open(OLDZONE,$target)){
while (<OLDZONE>) {
next unless (/(\d{8}).*serial/);
$oldserial = $1;
last;
}
close(OLDZONE);
}
else {
$oldserial = "000000";
}
$olddate = substr($oldserial,0,6);
$count = ($olddate == $today) ? substr($oldserial,6,2)+1 : 0;
$serial = sprintf("%6d%02d",$today,$count);
$header .= "; dns zone file - GENERATED BY $0\n";
$header .= "; DO NOT EDIT BY HAND!\n;\n";
$header .= "; Converted by $user on ".scalar(localtime)."\n;\n";
# count the number of entries in each department and then report
foreach $entry (keys %entries){
$depts{$entries{$entry}->{department}}++;
}
foreach $dept (keys %depts) {
$header .= "; number of hosts in the $dept department:
$depts{$dept}.\n";
}
$header .= "; total number of hosts: ".scalar(keys %entries)."\n#\n\n";
$header .= <<"EOH";
@ IN SOA dns.oog.org. hostmaster.oog.org. (
$serial ; serial
10800 ; refresh
3600 ; retry
604800 ; expire
43200) ; TTL
@ IN NS dns.oog.org.
EOH
return $header;
}
sub byaddress {
@a = split(/\./,$entries{$a}->{address});
@b = split(/\./,$entries{$b}->{address});
($a[0]<=>$b[0]) ||
($a[1]<=>$b[1]) ||
($a[2]<=>$b[2]) ||
($a[3]<=>$b[3]);
}
Here's the forward mapping file (zone.db)
that gets created:
; dns zone file - GENERATED BY createdns
; DO NOT EDIT BY HAND!
;
; Converted by David N. Blank-Edelman (dnb); on Fri May 29 15:46:46 1998
;
; number of hosts in the design department: 1.
; number of hosts in the software department: 1.
; number of hosts in the IT department: 2.
; total number of hosts: 4
;
@ IN SOA dns.oog.org. hostmaster.oog.org. (
1998052900 ; serial
10800 ; refresh
3600 ; retry
604800 ; expire
43200) ; TTL
@ IN NS dns.oog.org.
; Owned by Cindy Coltrane (marketing): west/143
bendir IN A 192.168.1.3
ben IN CNAME bendir
bendoodles IN CNAME bendir
; Owned by David Davis (software): main/909
shimmer IN A 192.168.1.11
shim IN CNAME shimmer
shimmy IN CNAME shimmer
shimmydoodles IN CNAME shimmer
; Owned by Ellen Monk (design): main/1116
sulawesi IN A 192.168.1.12
sula IN CNAME sulawesi
su-lee IN CNAME sulawesi
; Owned by Alex Rollins (IT): main/1101
sander IN A 192.168.1.55
sandy IN CNAME sander
micky IN CNAME sander
mickydoo IN CNAME sander
And here's the reverse mapping file
(rev.db):
; dns zone file - GENERATED BY createdns
; DO NOT EDIT BY HAND!
;
; Converted by David N. Blank-Edelman (dnb); on Fri May 29 15:46:46 1998
;
; number of hosts in the design department: 1.
; number of hosts in the software department: 1.
; number of hosts in the IT department: 2.
; total number of hosts: 4
;
@ IN SOA dns.oog.org. hostmaster.oog.org. (
1998052900 ; serial
10800 ; refresh
3600 ; retry
604800 ; expire
43200) ; TTL
@ IN NS dns.oog.org.
; Owned by Cindy Coltrane (marketing): west/143
3 IN PTR bendir.oog.org.
; Owned by David Davis (software): main/909
11 IN PTR shimmer.oog.org.
; Owned by Ellen Monk (design): main/1116
12 IN PTR sulawesi.oog.org.
; Owned by Alex Rollins (IT): main/1101
55 IN PTR sander.oog.org.
This method of creating files opens up many more possibilities. Up to
now, we've generated files using content from a single text
file database. We read a record from the database and we write it out
to our file, perhaps with a dash of nice formatting. Only data that
appeared in the database found its way into the files we created.
Sometimes it is useful to have content added in the conversion
process by the script itself. For instance, in the case of DNS
configuration files generation, you may wish to embellish the
conversion script so it inserts MX (Mail eXchange) records pointing
to a central mail server for every host in your database. A trivial
code change from:
# print A record
printf OUTPUT "%-20s\tIN A %s\n",
$entries{$entry}->{name},$entries{$entry}->{address};
to:
# print A record
printf OUTPUT "%-20s\tIN A %s\n",
$entries{$entry}->{name},$entries{$entry}->{address};
# print MX record
print OUTPUT " IN MX 10 $mailserver\n";
will configure DNS so that mail destined for any host in the domain
is received by the machine $mailserver instead. If
that machine is configured to handle mail for its domain, we've
activated a really useful infrastructure component (i.e., centralized
mail handling) with just a single line of Perl code.
5.3.2. DNS Checking: An Iterative Approach
We've spent considerable time in this chapter on the creation
of the configuration information to be served by network name
services, but that's only one side of the coin for system and
network administrators. Keeping a network healthy also entails
checking these services once they're up and running to make
sure they are behaving in a correct and consistent manner.
For instance, for a system/network administrator, a great deal rides
on the question "Are all of my DNS servers up?" In a
troubleshooting situation, it's equally valuable to know
"Are they all serving the same information?", or, more
specifically, "Are the servers responding to the same queries
with the same responses? Are they in sync as intended?"
We'll put these questions to good use in this section.
In
Chapter 2, "Filesystems" we saw an example of the Perl motto
"There's More Than One Way To Do It." Perl's
TMTOWTDI-ness makes the language an excellent prototype language in
which to do "iterative development." Iterative
development is one way of describing the evolutionary process that
takes place when writing system administration (and other) programs
to handle a particular task. With Perl it's all too possible to
bang out a quick-and-dirty hack that gets a job done. Later on, you
may return to this script and re-write it so it is more elegant.
There's even likely to be yet a third iteration of the same
code, this time taking a different approach to solving the problem.
Here are three different approaches to the same problem of DNS
consistency checking. These approaches will be presented in the order
someone might realistically follow while trying to solve the problem
and refine the solution. This ordering reflects one view on how a
solution to a problem can evolve in Perl; your take on this may
differ. The third approach, using the Net::DNS
module, is probably the easiest and most error-proof of the bunch.
But Net::DNS may not address every situation, so
we're going to walk through some "roll your own"
approaches first. Be sure to note the pros and cons listed after each
solution has been presented.
Here's the task: write a Perl script that takes a hostname and
checks a list of DNS servers to see if they all return the same
information when queried about this host. To make this task simpler,
we're going to assume that the host has a single, static IP
address (i.e., does not have multiple interfaces or addresses
associated with it).
Before we look at each approach in turn, let me show you the
"driver" code we're going to use:
$hostname = $ARGV[0];
@servers = qw(nameserver1 nameserver2 nameserver3); # name servers
foreach $server (@servers) {
&lookupaddress($hostname,$server); # populates %results
}
%inv = reverse %results; # invert the result hash
if (keys %inv > 1) {
print "There is a discrepancy between DNS servers:\n";
use Data::Dumper;
print Data::Dumper->Dump([\%results],["results"]),"\n";
}
For each of the DNS servers listed in the @servers
list, we call the &lookupaddress( )
subroutine. &lookupaddress( ) queries a
specific DNS server for the IP address of a given hostname and places
the results into a hash called %results. Each DNS
server has a key in %results with the IP address
returned by that server as its value.
There are many ways to determine if all of the values in
%results are the same (i.e., all DNS servers
returned the same thing in response to our query). Here we choose to
invert %results into another hash table, making
all of the keys into values, and vice versa. If all values in
%results are the same, there should be exactly one
key in the inverted hash. If not, we know we've got a situation
on our hands, so we call Data::Dumper->Dump(
) to nicely display the contents of
%results for the system administrator to puzzle
over.
Here's a sample of what the output looks like when something
goes wrong:
There is a discrepancy between DNS servers:
$results = {
nameserver1 => '192.168.1.2',
nameserver2 => '192.168.1.5',
nameserver3 => '192.168.1.2',
};
Let's take a look at the contestants for the
&lookupaddress( ) subroutines.
5.3.2.1. Using nslookup
If your
background is in Unix, or you've done some programming in other
scripting languages besides Perl, your first attempt might look a
great deal like a shell script. An external program called from the
Perl script does the hard work in the following code:
use Data::Dumper;
$hostname = $ARGV[0];
$nslookup = "/usr/local/bin/nslookup"; # nslookup binary
@servers = qw(nameserver1 nameserver2 nameserver3); # name of the name servers
foreach $server (@servers) {
&lookupaddress($hostname,$server); # populates %results
}
%inv = reverse %results; # invert the result hash
if (scalar(keys %inv) > 1) {
print "There is a discrepancy between DNS servers:\n";
print Data::Dumper->Dump([\%results],["results"]),"\n";
}
# ask the server to look up the IP address for the host
# passed into this program on the command line, add info to
# the %results hash
sub lookupaddress {
my($hostname,$server) = @_;
open(NSLOOK,"$nslookup $hostname $server|") or
die "Unable to start nslookup:$!\n";
while (<NSLOOK>) {
# ignore until we hit "Name: "
next until (/^Name:/);
# next line is Address: response
chomp($results{$server} = <NSLOOK>);
# remove the field name
die "nslookup output error\n" unless /Address/;
$results{$server} =~ s/Address(es)?:\s+//;
# we're done with this nslookup
last;
}
close(NSLOOK);
}
The benefits of this approach are:
-
It's a short, quick program to write (perhaps even translated
line by line from a real shell script).
-
We did not have to write any messy network code.
-
It takes the Unix approach of using a general purpose language to
glue together other smaller, specialized programs to get a job done,
rather than creating a single monolithic program.
-
It may be the only approach for times when you can't code the
client-server communication in Perl; for instance, you have to talk
with a server that requires a special client and there's no
alternative.
The drawbacks of this approach are:
-
It's dependent on another program outside the script. What if
this program is not available? What if this program's output
format changes?
-
It's slower. It has to start up another process each time it
wants to make a query. We could have reduced this overhead by opening
a two-way pipe to an nslookup process that stays
running while we need it. This would take a little more coding skill,
but would be the right thing to do if we were going to continue down
this path and further enhance this code.
-
You have less control. We are at the external program's mercy
for implementation details. For instance, here
nslookup (more specifically the resolver library
nslookup is using) is handling server timeouts,
query retries, and appending a domain search list for us.
5.3.2.2. Working with raw network sockets
If
you are a "power sysadmin," you may decide calling
another program is not acceptable. You might want to implement the
DNS queries using nothing but Perl. This entails constructing network
packets by hand, shipping them out on the wire, and then parsing the
results returned from the server.
This is probably the most complicated code you'll find in this
entire book, written by looking at the reference sources described
below along with several examples of existing networking code
(including the module by Michael Fuhr we'll see in the next
section). Here is a rough overview of what is going on. Querying a
DNS server consists of constructing a specific network packet header
and packet contents, sending it to a DNS server, and then receiving
and parsing the response from that server.[5]
Each and every DNS packet (of the sort we are interested in) can have
up to five distinct sections:
-
Header
-
Contains flags and counters pertaining to the query or answer (always
present).
-
Question
-
Contains the question being asked of the server (present for a query
and echoed in a response).
-
Answer
-
Contains all the data for the answer to a DNS query (present in a DNS response packet).
-
Authority
-
Contains information on where an authoritative response may be
retrieved.
-
Additional
-
Contains any information the server wishes to return in addition to
the direct answer to a query.
Our
program will concern itself strictly with the first three of these.
We'll be using a set of pack( ) commands
to create the necessary data structure for a DNS packet header and
packet contents. We pass this data structure to the
IO::Socket module that handles sending this data
out as a packet. The same module will also listen for a response on
our behalf and return data for us to parse (using unpack(
)). Conceptually, this process is not very difficult.
There's one twist to this process that should be noted before
we look at the code. RFC1035 (Section 4.1.4) defines two ways of
representing domain names in DNS packets: uncompressed and
compressed. The uncompressed representation places the full domain
name (for example, host.oog.org) in the packet, and is
nothing special. But, if the same domain name is found more than once
in a packet, it is likely a compressed representation will be used
for everything but the first mention. A compressed representation
replaces the domain information or part of it with a two-byte pointer
back to the first uncompressed representation. This allows a packet
to mention host1,
host2, and host3 in longsubdomain.longsubdomain.oog.org,
without having to include the bytes for longsubdomain.longsubdomain.oog.org each
time. We have to handle both representations in our code, hence the
&decompress routine below. Without further
fanfare, here's the code:
use IO::Socket;
$hostname = $ARGV[0];
$defdomain = ".oog.org"; # default domain if not present
@servers = qw(nameserver1 nameserver2 nameserver3); # name of the name servers
foreach $server (@servers) {
&lookupaddress($hostname,$server); # populates %results
}
%inv = reverse %results; # invert the result hash
if (scalar(keys %inv) > 1) { # see how many elements it has
print "There is a discrepancy between DNS servers:\n";
use Data::Dumper;
print Data::Dumper->Dump([\%results],["results"]),"\n";
}
sub lookupaddress{
my($hostname,$server) = @_;
my($qname,$rname,$header,$question,$lformat,@labels,$count);
local($position,$buf);
###
### Construct the packet header
###
$header = pack("n C2 n4",
++$id, # query id
1, # qr, opcode, aa, tc, rd fields (only rd set)
0, # rd, ra
1, # one question (qdcount)
0, # no answers (ancount)
0, # no ns records in authority section (nscount)
0); # no addtl rr's (arcount)
# if we do not have any separators in the name of the host,
# append the default domain
if (index($hostname,'.') == -1) {
$hostname .= $defdomain;
}
# construct the qname section of a packet (domain name in question)
for (split(/\./,$hostname)) {
$lformat .= "C a* ";
$labels[$count++]=length;
$labels[$count++]=$_;
}
###
### construct the packet question section
###
$question = pack($lformat."C n2",
@labels,
0, # end of labels
1, # qtype of A
1); # qclass of IN
###
### send the packet to the server and read the response
###
$sock = new IO::Socket::INET(PeerAddr => $server,
PeerPort => "domain",
Proto => "udp");
$sock->send($header.$question);
# we're using UDP, so we know the max packet size
$sock->recv($buf,512);
close($sock);
# get the size of the response, since we're going to have to keep
# track of where we are in the packet as we parse it (via $position)
$respsize = length($buf);
###
### unpack the header section
###
($id,
$qr_opcode_aa_tc_rd,
$rd_ra,
$qdcount,
$ancount,
$nscount,
$arcount) = unpack("n C2 n4",$buf);
if (!$ancount) {
warn "Unable to lookup data for $hostname from $server!\n";
return;
}
###
### unpack the question section
###
# question section starts 12 bytes in
($position,$qname) = &decompress(12);
($qtype,$qclass)=unpack('@'.$position.'n2',$buf);
# move us forward in the packet to end of question section
$position += 4;
###
### unpack all of the resource record sections
###
for ( ;$ancount;$ancount--){
($position,$rname) = &decompress($position);
($rtype,$rclass,$rttl,$rdlength)=
unpack('@'.$position.'n2 N n',$buf);
$position +=10;
# this next line could be changed to use a more sophisticated
# data structure, it currently picks the last rr returned
$results{$server}=
join('.',unpack('@'.$position.'C'.$rdlength,$buf));
$position +=$rdlength;
}
}
# handle domain information that is "compressed" as per RFC1035
# we take in the starting position of our packet parse and return
# the name we found (after dealing with the compressed format pointer)
# and the place we left off in the packet at the end of the name we found
sub decompress {
my($start) = $_[0];
my($domain,$i,$lenoct);
for ($i=$start;$i<=$respsize;) {
$lenoct=unpack('@'.$i.'C', $buf); # get the length of label
if (!$lenoct){ # 0 signals we are done with this section
$i++;
last;
}
if ($lenoct == 192) { # we've been handed a pointer, so recurse
$domain.=(&decompress((unpack('@'.$i.'n',$buf) & 1023)))[1];
$i+=2;
last
}
else { # otherwise, we have a plain label
$domain.=unpack('@'.++$i.'a'.$lenoct,$buf).'.';
$i += $lenoct;
}
}
return($i,$domain);
}
Note that this code is not precisely equivalent to that from the
previous example because we're not trying to emulate all of the
nuances of nslookup's behavior (timeouts,
retries, searchlists, etc.). When looking at the three approaches
here, be sure to keep a critical eye out for these subtle
differences.
The benefits of this approach are:
-
It isn't dependent on any other programs. You don't need
to know the particulars of another programmer's work.
-
It may be as fast or faster than calling an external program.
-
It is easier to tweak the parameters of the situation (timeouts,
etc.).
The drawbacks of this approach are:
-
It's likely to take longer to write and is more complex than
the previous approach.
-
It requires more knowledge external to the direct problem at hand
(i.e., you may have to learn how to put DNS packets together by hand,
something we did not need to know when we called
nslookup).
-
You may have to handle OS-specific issues yourself (hidden in the
previous approach by the work already done by the external
program's author).
5.3.2.3. Using Net::DNS
As
mentioned in Chapter 1, "Introduction", one of Perl's real
strengths is the support of a large community of developers who churn
out code for others to reuse. If there's something you need to
do in Perl that seems universal, chances are good that someone else
has already written a module to handle it. In our case, we can make
use of Michael Fuhr's excellent Net::DNS
module to make our job simpler. For our task, we simply have to
create a new DNS resolver object, configure it with the name of the
DNS server we wish to use, ask it to send a query, and then use the
supplied methods to parse the response:
use Net::DNS;
@servers = qw(nameserver1 nameserver2 nameserver3); # name of the name servers
foreach $server (@servers) {
&lookupaddress($hostname,$server); # populates %results
}
%inv = reverse %results; # invert the result hash
if (scalar(keys %inv) > 1) { # see how many elements it has
print "There is a discrepency between DNS servers:\n";
use Data::Dumper;
print Data::Dumper->Dump([\%results],["results"]),"\n";
}
# only slightly modified from example in the Net::DNS manpage
sub lookupaddress{
my($hostname,$server) = @_;
$res = new Net::DNS::Resolver;
$res->nameservers($server);
$packet = $res->query($hostname);
if (!$packet) {
warn "Unable to lookup data for $hostname from $server!\n";
return;
}
# stores the last RR we receive
foreach $rr ($packet->answer) {
$results{$server}=$rr->address;
}
}
The benefits of this approach are:
-
The code is legible again.
-
It is often faster to write.
-
Depending on how the module you use is implemented (is it pure Perl
or is it glue to a set of C or C++ library calls?) the code you write
using this module may be just as fast as calling an external compiled
program.
-
It is potentially portable, depending on how much work the author of
the module has done for you. Any place this module can be installed,
your program can run.
-
As in the first approach we looked at, writing code can be quick and
easy if someone else has done the behind-the-scenes work for you. You
don't have to know how the module works; you just need to know
how to use it.
-
Code re-use. You are not reinventing the wheel each time.
The drawbacks of this approach are:
-
You are back in the dependency game. This time you need to make sure
this module is available for your program to run. You need to trust
that the module writer has done a decent job.
-
There may not be a module to do what you need, or it may not run on
the operating system of choice.
More often than not, a pre-written module is my preferred approach.
However, any of these approaches will get the job done. TMTOWTDI, so
go forth and do it!
 |  |  |
| 5.2. NIS, NIS+, and WINS |  | 5.4. Module Information for This Chapter |

Copyright © 2001 O'Reilly & Associates. All rights reserved.
|