 | |  |
15.2. C Programming with the Resolver Library Routines
Before writing any code, though, you
need to be familiar with the DNS message format and the resolver
library routines. In the shell script we just wrote,
nslookup parsed the DNS message. In a C program,
though, you have to do the parsing. Let's start this section on
programming by looking at the DNS message format.
15.2.1. DNS Message Format
You've seen the DNS message format
before, in Chapter 12, "nslookup and dig". It looks like this:
- Header section
- Question section
- Answer section
Authority section Additional section
The format of the header section is described in RFC
1035 on pages 26-28, and also in Appendix A, "DNS Message Format and Resource Records" of this
book. It looks like this:
query identification number (2 octets)
query response (1 bit)
opcode (4 bits)
authoritative answer (1 bit)
truncation (1 bit)
recursion desired (1 bit)
recursion available (1 bit)
reserved (3 bits)
response code (4 bits)
question count (2 octets)
answer record count (2 octets)
name server record count (2 octets)
additional record count (2 octets)
You'll also find opcode, response code, type, and class values
defined in arpa/nameser.h as well as routines to
extract this information from a message. We'll discuss these
routines, part of the name server library,
shortly.
The question section is described on
pages 28-29 of RFC 1035. It looks like this:
domain name (variable length)
query type (2 octets)
query class (2 octets)
The answer, authority, and
additional sections are described on pages 29-30 of RFC 1035. These
sections comprise some number of resource records that look like
this:
domain name (variable length)
type (2 octets)
class (2 octets)
TTL (4 octets)
resource data length (2 octets)
resource data (variable length)
The header section contains a count of how many of these resource
records are in each section.
15.2.2. Domain Name Storage
As you can see, the names
stored in the DNS message are of variable length. Unlike C, DNS does
not store the names as null-terminated strings. Domain names are
stored as a series of length/value pairs ending with an octet of
zero. Each label in a domain name is composed of a length octet and a
label. A name like venera.isi.edu is stored as:
6 venera 3 isi 3 edu 0
You can imagine how much of a DNS message could be devoted to storing
names. The developers of DNS recognized this and came up with a
simple way to compress domain names.
15.2.3. Domain Name Compression
Often, an entire domain name or, at
least, the trailing labels of a domain name match a name already
stored in the message. Domain name compression eliminates the
repetition of domain names by storing a pointer to the earlier
occurrence of the name instead of inserting the name again. Here is
how it works. Suppose a response message already contains the name
venera.isi.edu. If the name
vaxa.isi.edu is added to the
response, the label vaxa is
stored, and then a pointer to the earlier occurrence of isi.edu is added. So how are these
pointers implemented?
The first two bits of the
length octet indicate whether a
length/label pair or a pointer to a length/label pair follows. If the
first two bits are zero, then the length and label follow. As you may
remember from way back in Chapter 2, "How Does DNS Work?", a label is
limited to 63 characters. That's because the length field has
only the remaining six bits for the length of the label -- enough
to represent the lengths 0-63. If the first two bits of the length
octet are ones, then what follows is not a length but a pointer. The
pointer is the last six bits of the length octet
and the next octet -- 14 bits in total. The
pointer is an offset from the start of the DNS message. Now, when
vaxa.isi.edu is compressed
into a buffer containing only venera.isi.edu, this is what results:
byte offset: 0 123456 7 890 1 234 5 6 7890 1 2
-------------+--------------+--------
pkt contents: 6 venera 3 isi 3 edu 0 4 vaxa 0xC0 7
The 0xC0 is a byte with the high two bits ones
and the rest of the bits zeros. Since the high two bits are ones,
this is a pointer instead of a length. The pointer value is
seven -- the last six bits of the first octet are zeros and the
second octet is seven. At offset seven in this buffer, you find the
rest of the domain name that begins with vaxa, which is isi.edu.
In this example, we only showed compressing two domain names in a
buffer, not a whole DNS message. A DNS message would have had a
header as well as other fields. This example is intended only to give
you an idea of how the domain name compression works. Now the good
news: you don't really need to care how names are compressed as
long as the library routines do it properly. What you do need to know
is how parsing a DNS response message can get messed up if you are
off by one byte. For example, try to expand the name starting with
byte two instead of byte one. You'll discover that
"v" doesn't make a very good length octet or
pointer.
15.2.4. The Resolver Library Routines
The resolver library contains the
routines that you need to write your application. You'll use
these routines to generate queries. You'll use the
name server library routines, explained next, to
parse the response.
In case you're wondering why we're not using the BIND 9
resolver routines in our code, well, they haven't been written
yet. BIND 9 includes library routines to perform lots of powerful DNS
functions, but they're oriented toward the BIND 9's name
server's needs and are very complicated to use, we're
told. The developers tell us that a simpler resolver library is
coming and that in the meantime, we should use the BIND 8 resolver
library. A program linked against the BIND 8 library routines will
work just fine with a BIND 9 name server.
Here are the header files you must include:
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/nameser.h>
#include <resolv.h>
Now let's look at the resolver library routines.
int res_search(const char *dname,
int class,
int type,
u_char *answer,
int anslen) res_search is the "highest level"
resolver routine, and is called by
gethostbyname. res_search applies the
search algorithm to the domain name passed to it. That is, it takes
the domain name it receives ( dname),
"completes" the name (if it's not fully qualified)
by adding the various domain names from the resolver search list, and
calls res_query until it receives
a successful response, indicating that it found a valid, fully
qualified domain name. In addition to implementing the search
algorithm, res_search looks in
the file referenced by your HOSTALIASES environment variable. (The
HOSTALIASES variable was described in Chapter 6, "Configuring Hosts".)
So it also takes care of any "private" host aliases you
might have. res_search returns
the size of the response or fills in h_errno and
returns -1 if there was an error or the answer count is zero.
( h_errno is like errno, but
for DNS lookups.)Therefore, the only parameter that's really of interest to
res_search is dname
; the others are just passed through to res_query and the other resolver routines.
The other arguments are:
- class
- The class of the data you're looking up. This is almost always
the constant C_IN, the Internet class. The class constants are
defined in arpa/nameser.h.
- type
- The type of data you're looking up. Again, this is a constant
defined in arpa/ nameser.h. A typical value
would be T_NS to retrieve a name server record, or T_MX to retrieve
an MX record.
- answer
- A buffer in which res_search will
place the response message. Its size should be at least PACKETSZ
(from arpa/nameser.h) bytes.
- anslen
- The size of the answer buffer (e.g., PACKETSZ).
res_search returns the size of
the response or -1 if there was an error.
int res_query(const char *dname,
int class,
int type,
u_char *answer,
int anslen) res_query is one of the
"midlevel" resolver routines. It does all the real work
in looking up the domain name: it makes a query message by calling
res_mkquery, sends the query by
calling res_send, and looks at
enough of the response to determine whether your question was
answered. In many cases, res_query is called by res_search, which just feeds it the
different domain names to look up. As you'd expect, these two
functions have the same arguments. res_query returns the size of the response,
or it fills in h_errno and returns -1 if there
was an error or the answer count was zero.
int res_mkquery(int op,
const char *dname,
int class,
int type,
const u_char *data,
int datalen,
const u_char *newrr,
u_char *buf,
int buflen) res_mkquery creates the query message. It
fills in all the header fields, compresses the domain name into the
question section, and fills in the other question fields.The dname, class, and
type arguments are the same as for res_search and res_query. The remaining arguments are:
- op
- The "operation" to be performed. This is normally QUERY,
but it can be IQUERY (inverse query). However, as we've
explained before, IQUERY is seldom used. BIND Versions 4.9.4 and
later, by default, do not even support IQUERY.
- data
- A buffer containing the data for inverse queries. It is NULL when
op is QUERY.
- datalen
- The size of the data buffer. If data
is NULL, then datalen is zero.
- newrr
- A buffer used for the dynamic update code (covered in Chapter 10, "Advanced Features"). Unless you are playing with this feature, it
is always NULL.
- buf
- A buffer in which res_mkquery
places the query message. It should be PACKETSZ or larger,
like the answer buffer in res_search
and res_query.
- buflen
- The size of the buf buffer (e.g., PACKETSZ).
res_mkquery returns the size of
the query message or -1 if there was an error.
int res_send(const u_char *msg,
int msglen,
u_char *answer,
int anslen) res_send implements the retry algorithm. It
sends the query message, msg, in a UDP datagram,
but it can also send it over a TCP stream. The response message is
stored in answer. This routine, of all the
resolver routines, is the only one to use black magic (unless you
know all about connected datagram sockets). You've seen these
arguments before in the other resolver routines:
- msg
- The buffer containing the DNS query message.
- msglen
- The size of the message.
- answer
- The buffer in which to store the DNS response message.
- anslen
- The size of the answer message.
res_send returns the size of the
response or -1 if there was an error. If this routine returns -1 and
errno is ECONNREFUSED, then there is no name
server running on the target name server host.You can look at errno to see if it is
ECONNREFUSED after calling res_search
or res_query.
( res_search calls res_query, which calls res_send.) If you want to check
errno after calling res_query, clear errno
first. That way, you know the current call to res_send was the one that set
errno. However, you don't have to clear
errno before calling res_search. res_search clears errno
itself before calling res_query.
int res_init(void) res_init reads resolv.conf
and initializes a data structure called _res
(more about that later). All the previously discussed
routines will call res_init if
they detect that it hasn't been called previously. Or you can
call it on your own; this is useful if you want to change some of the
defaults before calling the first resolver library routine. If there
are any lines in resolv.conf that res_init doesn't understand, it
ignores them. res_init always
returns zero, even if the manpage reserves the right to return -1.
extern int h_errno;
int herror(const char *s) herror is a
routine like perror, except that it prints out a
string based on the value of the external variable h_errno
instead of errno. The only argument
is:
- s
- A string used to identify the error message. If a string
s is supplied, it is printed first, followed by
":" and a string based on the value of
h_errno.
Here are the possible values of h_errno:
- HOST_NOT_FOUND
- The domain name does not exist. The return code in the name server
response was NXDOMAIN.
- TRY_AGAIN
- Either the name server is not running, or the name server returned
SERVFAIL.
- NO_RECOVERY
- Either the domain name could not be compressed because it was an
invalid domain name (e.g., a name missing a label -- .movie.edu) or the name server returned
FORMERR, NOTIMP, or REFUSED.
- NO_DATA
- The domain name exists, but there is no data of the requested type.
- NETDB_INTERNAL
- There was a library error unrelated to the network or name service.
Instead, see errno for the problem
description.
15.2.5. The _res Structure
Each of the resolver routines (i.e.,
each routine whose name starts with res_) makes
use of a common data structure called _res. You
can change the behavior of the resolver routines by changing
_res. If you want to change the number of times
res_send retries a query, you can change the
value of the retry field. If you want to turn
off the resolver search algorithm, you turn off the RES_DNSRCH bit
from the options mask. You'll find the
all-important _res structure in
resolv.h:
struct _ _res_state {
int retrans; /* retransmission time interval */
int retry; /* number of times to retransmit */
u_long options; /* option flags - see below. */
int nscount; /* number of name servers */
struct sockaddr_in
nsaddr_list[MAXNS]; /* address of name server */
#define nsaddr nsaddr_list[0] /* for backward compatibility */
u_short id; /* current packet id */
char *dnsrch[MAXDNSRCH+1]; /* components of domain to search */
char defdname[MAXDNAME]; /* default domain */
u_long pfcode; /* RES_PRF_ flags - see below. */
unsigned ndots:4; /* threshold for initial abs. query */
unsigned nsort:4; /* number of elements in sort_list[] */
char unused[3];
struct {
struct in_addr addr; /* address to sort on */
u_int32_t mask;
} sort_list[MAXRESOLVSORT];
};
The options field is a simple bit mask of the
enabled options. To turn on a feature, turn on the corresponding bit
in the options field. Bit masks for each of the options are defined
in resolv.h; the options are:
- RES_INIT
- If this bit is on, then res_init
has been called.
- RES_DEBUG
- This bit causes resolver debugging messages to be printed, if the
resolver routines were compiled with DEBUG, that is. Off is the
default.
- RES_AAONLY
- Requires the answer to be authoritative, not from a name
server's cache. It's too bad this isn't
implemented, as it would be a useful feature. Given the BIND
resolver's design, this feature would have to be implemented in
the name server, and it's not.
- RES_PRIMARY
- Query the primary master name server only -- again, not
implemented.
- RES_USEVC
- Turn this bit on if you'd like the resolver to make its queries
over a virtual circuit (TCP) connection instead
of with UDP datagrams. As you might guess, there is a performance
penalty for setting up and tearing down a TCP connection. Off is the
default.
- RES_STAYOPEN
- If you are making your queries over a TCP connection, turning this
bit on causes the connection to be left open, so you can use it to
query the same remote name server again. Otherwise, the connection is
torn down after the query has been answered. Off is the default.
- RES_IGNTC
- If the name server response has the truncation bit set, then the
default resolver behavior is to retry the query using TCP. If this
bit is turned on, the truncation bit in the response message is
ignored and the query is not retried using TCP. Off is the default.
- RES_RECURSE
- The default behavior for the BIND resolver is to send recursive
queries. Turning this bit off turns off the "recursion
desired" bit in the query message. On is the default.
- RES_DEFNAMES
- The default behavior for the BIND resolver is to append the local
domain name to any domain name that does not have a dot in it.
Turning this bit off turns off appending the local domain name. On is
the default.
- RES_DNSRCH
- The default behavior for the BIND resolver is to append each element
of the search list to a domain name that does not end in a dot.
Turning this bit off turns off the search list function. On is the
default.
- RES_INSECURE1
- The default behavior for a 4.9.3 or later BIND resolver is to ignore
answers from name servers that were not queried. Turning this bit on
disables this security check. Off (i.e., security check on) is the
default.
- RES_INSECURE2
- The default behavior for a 4.9.3 or later BIND resolver is to ignore
answers in which the question section of the response does not match
the question section of the original query. Turning this bit on
disables this security check. Off (i.e., security check on) is the
default.
- RES_NOALIASES
- The default behavior for the BIND resolver
is to use aliases defined in the file specified by the user's
HOSTALIASES environment variable. Turning this bit on disables the
HOSTALIASES feature for 4.9.3 and later BIND resolvers. Previous
resolvers did not allow this feature to be disabled. Off is the
default.
- RES_USE_INET6
- Tells the resolver to return IPv6 addresses (in addition to IPv4
addresses) to the gethostbyname
function.
- RES_ROTATE
- Normally, a resolver that sends repeated queries always queries the
first name server in resolv.conf first. With
RES_ROTATE set, a BIND 8.2 or later resolver sends its first query to
the first name server in resolv.conf, its second
to the second name server, and so on. See the options
rotate directive in Chapter 6, "Configuring Hosts" for
details. The default is not to rotate name servers.
- RES_NOCHECKNAME
- Since BIND 4.9.4, resolvers have checked the domain names in
responses to make sure they conform to the naming guidelines
described in Chapter 4, "Setting Up BIND". BIND 8.2 resolvers offer
the option of turning the name checking mechanism off. Off (i.e.,
name check on) is the default.
- RES_KEEPTSIG
- This option tells a BIND 8.2 or later resolver not to strip the TSIG
record from a signed DNS message. This way, the application that
called the resolver can examine it.
- RES_BLAST
- "Blast" all recursive servers by sending queries to them
simultaneously. Not implemented yet.
- RES_DEFAULT
- This isn't a single option, but rather a combination of the
RES_RECURSE, RES_DEFNAMES, and RES_DNSRCH options, all of which are
on by default. You normally won't need to set RES_DEFAULT
explicitly; it's set for you when you call res_init.
15.2.6. The Name Server Library Routines
The name server library
contains routines you need to parse response messages. Here are the
header files you must include:
#include <sys/types.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/nameser.h>
#include <resolv.h>
Following are the name server library routines.
int ns_initparse(const u_char *msg,
int msglen,
ns_msg *handle) ns_initparse
is the first routine you must call before you use the
other name server library routines. ns_initparse fills in the data structure
pointed to by handle, which is a parameter
passed to other routines. The arguments are:
- msg
- A pointer to the beginning of the response message buffer.
- msglen
- The size of the message buffer.
- handle
- A pointer to a data structure filled in by ns_initparse.
ns_initparse returns zero on
success and -1 if it fails to parse the message buffer.
| ns_msg_base, ns_msg_end, and ns_msg_size | |
const u_char *ns_msg_base(ns_msg handle)
const u_char *ns_msg_end(ns_msg handle)
int ns_msg_size(ns_msg handle)
These routines return a pointer to the start of the message, a
pointer to the end of the message, and the size of the message. They
return the data you passed into ns_initparse. The only argument is:
- handle
- A data structure filled in by ns_initparse.
u_int16_t ns_msg_id(ns_msg handle) ns_msg _id returns the identification from
the header section (described earlier) of the response message. The
only argument is:
- handle
- A data structure filled in by ns_initparse.
u_int16_t ns_msg_get_flag(ns_msg handle, ns_flag flag) ns_msg _ get_
flag returns the "flag" fields from the header
section of the response message. Its arguments are:
- handle
- A data structure filled in by ns_initparse.
- flag
- An enumerated type that can have the following values:
ns_f_qr /* Question/Response */
ns_f_opcode /* Operation Code */
ns_f_aa /* Authoritative Answer */
ns_f_tc /* Truncation Occurred */
ns_f_rd /* Recursion Desired */
ns_f_ra /* Recursion Available */
ns_f_z /* Must Be Zero */
ns_f_ad /* Authentic Data (DNSSEC) */
ns_f_cd /* Checking Disabled (DNSSEC) *
ns_f_rcode /* Response Code */
ns_f_max
u_int16_t ns_msg_count(ns_msg handle, ns_sect section) ns_msg_count
returns a counter from the header section of the response
message. Its arguments are:
- handle
- A data structure filled in by ns_initparse.
- section
- An enumerated type that can have the following values:
ns_s_qd /* Query: Question section */
ns_s_zn /* Update: Zone section */
ns_s_an /* Query: Answer section */
ns_s_pr /* Update: Prerequisite section */
ns_s_ns /* Query: Name Server section */
ns_s_ud /* Update: Update section */
ns_s_ar /* Query|Update: Additional records section
*/
int ns_parserr(ns_msg *handle,
ns_sect section,
int rrnum,
ns_rr *rr) ns_parserr
extracts information about a response record and stores it
in rr, whichis a parameter
passed to other name server libarary routines. The arguments are:
- handle
- A pointer to a data structure filled in by ns_initparse.
- section
- The same parameter described in ns_msg
_count.
- rrnum
- A resource record number for the resource records in this section.
Resource records start numbering at zero. ns_msg _count
tells you how many resource records are in this section.
- rr
- A pointer to a data structure to be initialized.
ns_parserr returns zero on
success and -1 if it fails to parse the response buffer.
char *ns_rr_name(ns_rr rr)
u_int16_t ns_rr_type(ns_rr rr)
u_int16_t ns_rr_class(ns_rr rr)
u_int32_t ns_rr_ttl(ns_rr rr)
u_int16_t ns_rr_rdlen(ns_rr rr)
const u_char *ns_rr_rdata(ns_rr rr)
These routines return individual fields from a response record. Their
only argument is:
- rr
- A data structure filled in by ns_parserr.
int ns_name_compress(const char *exp_dn,
u_char *comp_dn,
size_t length,
const u_char **dnptrs,
const u_char **lastdnptr) ns_name_compress compresses a domain name.
You won't normally call this routine
yourself -- you'll let res_mkquery
do it for you. However, if you need to compress a name for
some reason, this is the tool to do it. The arguments are:
- exp_dn
- The "expanded" domain name that you supply; i.e., a
normal, null-terminated string containing a fully qualified domain
name.
- comp_dn
- The place where ns_name_compress
will store the compressed domain name.
- length
- The size of the comp_dn buffer.
- dnptrs
- An array of pointers to previously compressed domain names.
dnptrs[0] points to the beginning of the
message; the list ends with a NULL pointer. After you've
initialized dnptrs[0] to the beginning of the
message and dnptrs[1] to NULL, dn_comp
updates the list each time you call it.
- lastdnptr
- A pointer to the end of the dnptrs array.
ns_name_compress needs to know
where the end of the array is so it doesn't overrun it.
If you want to use this routine, look at how it is used in the BIND
source in src/lib/resolv/res_mkquery.c (BIND 8)
or res/res_mkquery.c (BIND 4). It's often
easier to see how to use a routine from an example than from an
explanation. ns_name_compress
returns the size of the compressed name or -1 if there was an error.
int ns_name_uncompress(const u_char *msg,
const u_char *eomorig,
const u_char *comp_dn,
char *exp_dn,
size_t length) ns_name_uncompress
expands a "compressed" domain name.
You'll use this routine if you parse a name server response
message, as we do in check_soa, the C program
that follows. The arguments are:
- msg
- A pointer to the beginning of your response message.
- eomorig
- A pointer to the first byte after the message. It is used to make
sure that ns_name_uncompress
doesn't go past the end of the message.
- comp_dn
- A pointer to the compressed domain name within the message.
- exp_dn
- The place where ns_name_uncompress
will store the expanded name. You should always allocate
an array of MAXDNAME characters for the expanded name.
- length
- The size of the exp_dn buffer.
ns_name_uncompress returns the
size of the compressed name or -1 if there was an error. You might
wonder why ns_name_uncompress
returns the size of the compressed
name, not the size of the expanded
name. It does this because when you call ns_name_uncompress, you are parsing a DNS
message and need to know how much space the compressed name took in
the message so that you can skip over it.
int ns_name_skip(const u_char **ptrptr, const u_char *eom) ns_name_skip
is like ns_name_uncompress, but instead of
uncompressing the name, it just skips over it. The arguments are:
- ptrptr
- A pointer to a pointer to the name to skip over. The original pointer
is advanced past the name.
- eom
- A pointer to the first byte after the message. It is used to make
sure that ns_name_skip
doesn't go past the end of the message.
ns_name_skip returns zero if
successful. It returns -1 if it fails to uncompress the name.
u_int ns_get16(const u_char *cp)
void ns_put16(u_int s, u_char *cp)
The DNS messages have fields that are unsigned short integer (type,
class, and data length, to name a few). ns_get16 returns a 16-bit integer pointed
to by cp, and ns_put16
assigns the 16-bit value of s to the
location pointed to by cp.
u_long ns_get32(const u_char *cp)
void ns_put32(u_long l, u_char *cp)
These routines are like their 16-bit
counterparts except that they deal with a 32-bit integer instead of a
16-bit integer. The TTL (time to live) field of a resource record is a
32-bit
integer.
15.2.7. Parsing DNS Responses
The easiest way to learn how to
parse a DNS message is to look at code that already does it. Assuming
that you have the BIND source code, the best file to look through is
src/lib/resolv/res_debug.c (BIND 8) or
res/res_debug.c (BIND 4). (If you're
really determined to use BIND 9, you might have to read almost 3000
lines of lib/dns/message.c.)
res_debug.c contains fp_query (or res_pquery in BIND 8.2 and later), the
function that prints out the DNS messages in the name server
debugging output. Our sample program traces its parentage to code
from this file.
You won't always want to parse the DNS response manually. An
"intermediate" way to parse the response is to call
p_query, which calls fp_query, to print out the DNS message.
Then use Perl or awk to grab what you need.
Cricket has been known to wimp out this way.
15.2.8. A Sample Program: check_soa
Let's now look at a C program to solve
the same problem for which we wrote a shell script earlier.
Here are the header files that are needed, the declarations for
external variables, and the declarations of functions. Notice that we
use both h_errno (for the resolver routines) and
errno. We limit this program to checking 20 name
servers. You'll rarely see a zone with more than 10 name
servers, so an upper limit of 20 should suffice:
/****************************************************************
* check_soa -- Retrieve the SOA record from each name server *
* for a given zone and print out the serial number. *
* *
* usage: check_soa zone *
* *
* The following errors are reported: *
* o There is no address for a server. *
* o There is no server running on this host. *
* o There was no response from a server. *
* o The server is not authoritative for the zone. *
* o The response had an error response code. *
* o The response had more than one answer. *
* o The response answer did not contain an SOA record. *
* o The expansion of a compressed domain name failed. *
****************************************************************/
/* Various header files */
#include <sys/types.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <errno.h>
#include <arpa/nameser.h>
#include <resolv.h>
/* Error variables */
extern int h_errno; /* for resolver errors */
extern int errno; /* general system errors */
/* Our own routines; code included later in this chapter */
void nsError( ); /* report resolver errors */
void findNameServers( ); /* find a zone's name servers */
void addNameServers( ); /* add name servers to our list */
void queryNameServers( ); /* grab SOA records from servers */
void returnCodeError( ); /* report response message errors */
/* Maximum number of name servers we will check */
#define MAX_NS 20
The main body of the program is small. We have an array of string
pointers, nsList, to store the names of the name
servers for the zone. We call the resolver function res_init to initialize the _res
structure. It wasn't necessary for this program to
call res_init explicitly since it
would have been called by the first resolver routine that used the
_res structure. However, if we had wanted to
modify the value of any of the _res fields
before calling the first resolver routine, we would have made the
modifications right after calling res_init. Next, the program calls findNameServers to find all the name
servers for the zone referenced in argv[1] and
to store them in nsList. Last, the program calls
queryNameServers to query each of
the name servers in nsList for the SOA record
for the zone:
main(argc, argv)
int argc;
char *argv[];
{
char *nsList[MAX_NS]; /* list of name servers */
int nsNum = 0; /* number of name servers in list */
/* sanity check: one (and only one) argument? */
if(argc != 2){
(void) fprintf(stderr, "usage: %s zone\n", argv[0]);
exit(1);
}
(void) res_init( );
/*
* Find the name servers for the zone.
* The name servers are written into nsList.
*/
findNameServers(argv[1], nsList, &nsNum);
/*
* Query each name server for the zone's SOA record.
* The name servers are read from nsList.
*/
queryNameServers(argv[1], nsList, nsNum);
exit(0);
}
The routine findNameServers
follows. This routine queries the local name server for
the NS records for the zone. It then calls addNameServers to parse the response
message and store away all the name servers it finds. The header
files, arpa/nameser.h and
resolv.h, contain declarations we make extensive
use of:
/****************************************************************
* findNameServers -- find all of the name servers for the *
* given zone and store their names in nsList. nsNum is *
* the number of servers in the nsList array. *
****************************************************************/
void
findNameServers(domain, nsList, nsNum)
char *domain;
char *nsList[];
int *nsNum;
{
union {
HEADER hdr; /* defined in resolv.h */
u_char buf[NS_PACKETSZ]; /* defined in arpa/nameser.h */
} response; /* response buffers */
int responseLen; /* buffer length */
ns_msg handle; /* handle for response message */
/*
* Look up the NS records for the given domain name.
* We expect the domain name to be a fully qualified, so
* we use res_query( ). If we'd wanted the resolver search
* algorithm, we would have used res_search( ) instead.
*/
if((responseLen =
res_query(domain, /* the zone we care about */
ns_c_in, /* Internet class records */
ns_t_ns, /* Look up name server records*/
(u_char *)&response, /*response buffer*/
sizeof(response))) /*buffer size */
< 0){ /*If negative */
nsError(h_errno, domain); /* report the error */
exit(1); /* and quit */
}
/*
* Initialize a handle to this response. The handle will
* be used later to extract information from the response.
*/
if (ns_initparse(response.buf, responseLen, &handle) < 0) {
fprintf(stderr, "ns_initparse: %s\n", strerror(errno));
return;
}
/*
* Create a list of name servers from the response.
* NS records may be in the answer section and/or in the
* authority section depending on the DNS implementation.
* Walk through both. The name server addresses may be in
* the additional records section, but we will ignore them
* since it is much easier to call gethostbyname( ) later
* than to parse and store the addresses here.
*/
/*
* Add the name servers from the answer section.
*/
addNameServers(nsList, nsNum, handle, ns_s_an);
/*
* Add the name servers from the authority section.
*/
addNameServers(nsList, nsNum, handle, ns_s_ns);
}
/****************************************************************
* addNameServers -- Look at the resource records from a *
* section. Save the names of all name servers. *
****************************************************************/
void
addNameServers(nsList, nsNum, handle, section)
char *nsList[];
int *nsNum;
ns_msg handle;
ns_sect section;
{
int rrnum; /* resource record number */
ns_rr rr; /* expanded resource record */
int i, dup; /* misc variables */
/*
* Look at all the resource records in this section.
*/
for(rrnum = 0; rrnum < ns_msg_count(handle, section); rrnum++)
{
/*
* Expand the resource record number rrnum into rr.
*/
if (ns_parserr(&handle, section, rrnum, &rr)) {
fprintf(stderr, "ns_parserr: %s\n", strerror(errno));
}
/*
* If the record type is NS, save the name of the
* name server.
*/
if (ns_rr_type(rr) == ns_t_ns) {
/*
* Allocate storage for the name. Like any good
* programmer should, we test malloc's return value,
* and quit if it fails.
*/
nsList[*nsNum] = (char *) malloc (MAXDNAME);
if(nsList[*nsNum] == NULL){
(void) fprintf(stderr, "malloc failed\n");
exit(1);
}
/* Expand the name server's domain name */
if (ns_name_uncompress(
ns_msg_base(handle),/* Start of the message */
ns_msg_end(handle), /* End of the message */
ns_rr_rdata(rr), /* Position in the message */
nsList[*nsNum], /* Result */
MAXDNAME) /* Size of nsList buffer */
< 0) { /* Negative: error */
(void) fprintf(stderr, "ns_name_uncompress failed\n");
exit(1);
}
/*
* Check the domain name we've just unpacked and add it to
* the list of name servers if it is not a duplicate.
* If it is a duplicate, just ignore it.
*/
for(i = 0, dup=0; (i < *nsNum) && !dup; i++)
dup = !strcasecmp(nsList[i], nsList[*nsNum]);
if(dup)
free(nsList[*nsNum]);
else
(*nsNum)++;
}
}
}
Notice that we don't explicitly check for finding zero name
server records. We don't need to check
because
res_query flags that case as an
error; it returns -1 and sets herrno to
NO_DATA. If res_query
returns -1, we call our own routine, nsError, to print out an error string from
h_errno instead of using herror. The herror
routine isn't a good fit for our program because its
messages assume you are looking up address data (e.g., if
h_ errno is NO_DATA, the
error message is "No address associated with name").
The next routine queries each name server that we've found for
an SOA record. In this routine, we change the value of several of the
_res structure fields. By changing the
nsaddr_list field, we change which name server
res_send queries. We disable the
search list by turning off bits in the options
field -- all the domain names that this program handles
are fully qualified:
/******************************************************************
* queryNameServers -- Query each of the name servers in nsList *
* for the SOA record of the given zone. Report any *
* errors encountered (e.g., a name server not running or *
* the response not being an authoritative response). If *
* there are no errors, print out the serial number for the zone. *
******************************************************************/
void
queryNameServers(domain, nsList, nsNum)
char *domain;
char *nsList[];
int nsNum;
{
union {
HEADER hdr; /* defined in resolv.h */
u_char buf[NS_PACKETSZ]; /* defined in arpa/nameser.h */
} query, response; /* query and response buffers */
int responseLen, queryLen; /* buffer lengths */
u_char *cp; /* character pointer to parse DNS message */
struct in_addr saveNsAddr[MAXNS]; /* addrs saved from _res */
int nsCount; /* count of addresses saved from _res */
struct hostent *host; /* structure for looking up ns addr */
int i; /* counter variable */
ns_msg handle; /* handle for response message */
ns_rr rr; /* expanded resource record */
/*
* Save the _res name server list since
* we will need to restore it later.
*/
nsCount = _res.nscount;
for(i = 0; i < nsCount; i++)
saveNsAddr[i] = _res.nsaddr_list[i].sin_addr;
/*
* Turn off the search algorithm and turn off appending
* the local domain name before we call gethostbyname( );
* the name server's domain names will be fully qualified.
*/
_res.options &= ~(RES_DNSRCH | RES_DEFNAMES);
/*
* Query each name server for the zone's SOA record.
*/
for(nsNum-- ; nsNum >= 0; nsNum--){
/*
* First, we have to get the IP address of every name server.
* So far, all we have are domain names. We use gethostbyname( )
* to get the addresses, rather than anything fancy.
* But first, we have to restore certain values in _res
* because _res affects gethostbyname( ). (We altered
* _res in the previous iteration through the loop.)
*
* We can't just call res_init( ) again to restore
* these values since some of the _res fields are
* initialized when the variable is declared, not when
* res_init( ) is called.
*/
_res.options |= RES_RECURSE; /* recursion on (default) */
_res.retry = 4; /* 4 retries (default) */
_res.nscount = nsCount; /* original name servers */
for(i = 0; i < nsCount; i++)
_res.nsaddr_list[i].sin_addr = saveNsAddr[i];
/* Look up the name server's address */
host = gethostbyname(nsList[nsNum]);
if (host == NULL) {
(void) fprintf(stderr,"There is no address for %s\n",
nsList[nsNum]);
continue; /* nsNum for-loop */
}
/*
* Now get ready for the real fun. host contains IP
* addresses for the name server we're testing.
* Store the first address for host in the _res
* structure. Soon, we'll look up the SOA record...
*/
(void) memcpy((void *)&_res.nsaddr_list[0].sin_addr,
(void *)host->h_addr_list[0], (size_t)host->h_length);
_res.nscount = 1;
/*
* Turn off recursion. We don't want the name server
* querying another server for the SOA record; this name
* server ought to be authoritative for this data.
*/
_res.options &= ~RES_RECURSE;
/*
* Reduce the number of retries. We may be checking
* several name servers, so we don't want to wait too
* long for any one server. With two retries and only
* one address to query, we'll wait at most 15 seconds.
*/
_res.retry = 2;
/*
* We want to see the response code in the next
* response, so we must make the query message and
* send it ourselves instead of having res_query( )
* do it for us. If res_query( ) returned -1, there
* might not be a response to look at.
*
* There is no need to check for res_mkquery( )
* returning -1. If the compression was going to
* fail, it would have failed when we called
* res_query( ) earlier with this domain name.
*/
queryLen = res_mkquery(
ns_o_query, /* regular query */
domain, /* the zone to look up */
ns_c_in, /* Internet type */
ns_t_soa, /* look up an SOA record */
(u_char *)NULL, /* always NULL */
0, /* length of NULL */
(u_char *)NULL, /* always NULL */
(u_char *)&query,/* buffer for the query */
sizeof(query)); /* size of the buffer */
/*
* Send the query message. If there is no name server
* running on the target host, res_send( ) returns -1
* and errno is ECONNREFUSED. First, clear out errno.
*/
errno = 0;
if((responseLen = res_send((u_char *)&query,/* the query */
queryLen, /* true length*/
(u_char *)&response,/*buffer */
sizeof(response))) /*buf size*/
< 0){ /* error */
if(errno == ECONNREFUSED) { /* no server on the host */
(void) fprintf(stderr,
"There is no name server running on %s\n",
nsList[nsNum]);
} else { /* anything else: no response */
(void) fprintf(stderr,
"There was no response from %s\n",
nsList[nsNum]);
}
continue; /* nsNum for-loop */
}
/*
* Initialize a handle to this response. The handle will
* be used later to extract information from the response.
*/
if (ns_initparse(response.buf, responseLen, &handle) < 0) {
fprintf(stderr, "ns_initparse: %s\n", strerror(errno));
return;
}
/*
* If the response reports an error, issue a message
* and proceed to the next server in the list.
*/
if(ns_msg_getflag(handle, ns_f_rcode) != ns_r_noerror){
returnCodeError(ns_msg_getflag(handle, ns_f_rcode),
nsList[nsNum]);
continue; /* nsNum for-loop */
}
/*
* Did we receive an authoritative response? Check the
* authoritative answer bit. If this name server isn't
* authoritative, report it, and go on to the next server.
*/
if(!ns_msg_getflag(handle, ns_f_aa)){
(void) fprintf(stderr,
"%s is not authoritative for %s\n",
nsList[nsNum], domain);
continue; /* nsNum for-loop */
}
/*
* The response should only contain one answer; if more,
* report the error, and proceed to the next server.
*/
if(ns_msg_count(handle, ns_s_an) != 1){
(void) fprintf(stderr,
"%s: expected 1 answer, got %d\n",
nsList[nsNum], ns_msg_count(handle, ns_s_an));
continue; /* nsNum for-loop */
}
/*
* Expand the answer section record number 0 into rr.
*/
if (ns_parserr(&handle, ns_s_an, 0, &rr)) {
if (errno != ENODEV){
fprintf(stderr, "ns_parserr: %s\n",
strerror(errno));
}
}
/*
* We asked for an SOA record; if we got something else,
* report the error and proceed to the next server.
*/
if (ns_rr_type(rr) != ns_t_soa) {
(void) fprintf(stderr,
"%s: expected answer type %d, got %d\n",
nsList[nsNum], ns_t_soa, ns_rr_type(rr));
continue; /* nsNum for-loop */
}
/*
* Set cp to point the the SOA record.
*/
cp = (u_char *)ns_rr_rdata(rr);
/*
* Skip the SOA origin and mail address, which we don't
* care about. Both are standard "compressed names."
*/
ns_name_skip(&cp, ns_msg_end(handle));
ns_name_skip(&cp, ns_msg_end(handle));
/* cp now points to the serial number; print it. */
(void) printf("%s has serial number %d\n",
nsList[nsNum], ns_get32(cp));
} /* end of nsNum for-loop */
}
Notice that we use recursive queries when we call gethostbyname, but nonrecursive queries
when we look up the SOA record. gethostbyname may need to query other name
servers to find the host's address. But we don't want the
name server querying another server when we ask it for the SOA
record -- it's supposed to be
authoritative for this zone, after all. Allowing the name server to
ask another server for the SOA record would defeat the error check.
The next two routines print out error messages:
/****************************************************************
* nsError -- Print an error message from h_errno for a failure *
* looking up NS records. res_query( ) converts the DNS *
* message return code to a smaller list of errors and *
* places the error value in h_errno. There is a routine *
* called herror( ) for printing out strings from h_errno *
* like perror( ) does for errno. Unfortunately, the *
* herror( ) messages assume you are looking up address *
* records for hosts. In this program, we are looking up *
* NS records for zones, so we need our own list of error *
* strings. *
****************************************************************/
void
nsError(error, domain)
int error;
char *domain;
{
switch(error){
case HOST_NOT_FOUND:
(void) fprintf(stderr, "Unknown zone: %s\n", domain);
break;
case NO_DATA:
(void) fprintf(stderr, "No NS records for %s\n", domain);
break;
case TRY_AGAIN:
(void) fprintf(stderr, "No response for NS query\n");
break;
default:
(void) fprintf(stderr, "Unexpected error\n");
break;
}
}
/****************************************************************
* returnCodeError -- print out an error message from a DNS *
* response return code. *
****************************************************************/
void
returnCodeError(rcode, nameserver)
ns_rcode rcode;
char *nameserver;
{
(void) fprintf(stderr, "%s: ", nameserver);
switch(rcode){
case ns_r_formerr:
(void) fprintf(stderr, "FORMERR response\n");
break;
case ns_r_servfail:
(void) fprintf(stderr, "SERVFAIL response\n");
break;
case ns_r_nxdomain:
(void) fprintf(stderr, "NXDOMAIN response\n");
break;
case ns_r_notimpl:
(void) fprintf(stderr, "NOTIMP response\n");
break;
case ns_r_refused:
(void) fprintf(stderr, "REFUSED response\n");
break;
default:
(void) fprintf(stderr, "unexpected return code\n");
break;
}
}
To compile this program using the resolver and name server routines
in libc:
% cc -o check_soa check_soa.c
Or, if you've newly compiled the BIND code as we describe in
Appendix C, "Compiling and Installing BIND on Linux", and want to use the latest header files
and resolver library:
% cc -o check_soa -I/usr/local/src/bind/src/include \
check_soa.c /usr/local/src/bind/src/lib/libbind.a
Here is what the output looks like:
% check_soa mit.edu
BITSY.MIT.EDU has serial number 1995
W20NS.MIT.EDU has serial number 1995
STRAWB.MIT.EDU has serial number 1995
If you look back at the shell script output, it looks the same,
except that the shell script's output is sorted by the name
server's name. What you can't see is that the C program
ran
much
faster.
 |  |  | | 15. Programming with the Resolver and Name Server Library Routines |  | 15.3. Perl Programming with Net::DNS |
|