From caef8958b57c221a60ac7992cbd77c2b9a48199d Mon Sep 17 00:00:00 2001 From: short <> Date: Fri, 28 Dec 2001 20:44:42 +0000 Subject: [PATCH 1/1] ftp://atrey.karlin.mff.cuni.cz/pub/local/mj/net/sleuth-1.3.tar.gz --- ChangeLog | 146 +++++++++++++ Makefile | 36 ++++ README | 169 +++++++++++++++ THANKS | 14 ++ TODO | 8 + check.cgi | 192 +++++++++++++++++ check.conf | 26 +++ sleuth | 677 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ sleuth.conf | 102 +++++++++ 9 files changed, 1370 insertions(+) create mode 100644 ChangeLog create mode 100644 Makefile create mode 100644 README create mode 100644 THANKS create mode 100644 TODO create mode 100755 check.cgi create mode 100644 check.conf create mode 100755 sleuth create mode 100644 sleuth.conf diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..ebf9948 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,146 @@ +2001-06-16 Martin Mares + + * sleuth (check_name): "IP address found instead of name" check + added to clarify most "all-digit name" error messages. + + * sleuth (check_reverse): Corrected references to RFC 1912. Also fixed + the private address check and turned it to a warning. + + * sleuth (check_zone): SRV records with empty destination and wildcard + SRV records are valid. + + * sleuth (check_zone): Better checks for wildcard records, no more + false alarms. + + * sleuth (resolve): Added authoritative answer checks (required + f.e. for localhost records). + + * Released as version 1.3. + +2001-06-15 Martin Mares + + * sleuth: Fixed a small bug in switching of nameservers. + Nameserver sanity check messages now indicate which nameserver + we're testing. Comparison of origin servers etc. is now really + case insensitive. + +2001-06-14 Martin Mares + + * check.cgi: Declare non-transitional DTD. We still use a couple + of transitional attributes (mostly align=center), but we don't + want the extra work-arounds Mozilla based browsers apply to + transitional documents. + + * sleuth (html_output), check.cgi: Revamped all HTML output stuff. + Now we're using style sheets to add colors and most of the alignment. + Works wonderfully in Mozilla, relatively good in non-CSS browsers, + a bit funny in Netscape 4 due to its bugs. + + * Released as version 1.2. + +2001-06-13 Martin Mares + + * check.cgi: Minor design changes. + + * sleuth (check_zone): Changed checks for minimum TTL according to RFC 2308 + which specifies minttl should be used to control negative caching. + + * sleuth (check_zone): Check duplicate records. + + * sleuth (check_zone): Dangling CNAME's in reverse zones produce only + warnings as they are usually an artifact of classless delegation schemes. + + * sleuth (check_zone): Avoid `PTR -> A for same address' checks when + not in reverse check mode, but always check there is at least one A. + + * sleuth (check_zone_name): Better parsing of reverse zone names, + give an error message if it fails. + + * sleuth (check_zone): Added checks of SRV records. + + * sleuth (check_zone): SOA: don't forget to resolve and check origin server. + + * sleuth (check_name): Relaxed the name checking rules to allow underscores. + No standard currently seems to specify what is the exact syntax of a host + name (only RFC 1033, but it's categorized as informational, not as a standard). + Strictly speaking, we should do separate checks for host names, mail names, + domain names etc., but I'd like to avoid such extra complexity for now. + + * sleuth (check_email): Warn about A records used instead of MX records. + +2001-06-12 Martin Mares + + * sleuth (check_zone_basics): Completely rewrote nameserver scans. All + nameservers mentioned in NS records plus the zone origin announced in + SOA are tried, the origin server is preferred. In case any of these + servers fails, the next one is tried automatically. If the user specifies + server name explicitly, do the basic checks for all nameservers, but + force use of the specified one for zone transfer and use it as the reference + name server. Also check differences between NS record sets reported by + all the servers. + + * sleuth (check_submit): Moved all submit-dependent checks here. Better + checking of top-level domain names. + + * sleuth (resolve): If the name requested is invalid, don't attempt + to resolve it. + + * sleuth: Added a "-p" switch for scanning of private networks which avoids + private IP address checks and connectivity checks. + + * sleuth (check_reverse): Report private IP addresses. + + * sleuth: Try to avoid cascading of some kinds of errors, especially + those induced by bogus CNAME's. + + * sleuth (check_zone): Wildcard A's and CNAME's are allowed, but strongly + deprecated. Tolerate PTR's in forward zones and A's in reverse zones, but + warn of them (they are permitted by RFC's, but this behaviour is very + obscure and it should be avoided) and check them anyway. + + * sleuth (check_reverse): Rewrote the reverse mapping checks. Removed the + "$recursive_check" machinery, it was unnecessary. Report all mispointed + PTR's. Tolerate PTR's to a different name, but warn on them. Don't check + reverse mapping of any IP address twice. + + * sleuth (check_zone): Better checks for recursive and overlapping CNAME's. + + * sleuth (check_email): Check all MX'es, not only the best one. + + * sleuth (check_zone): Fixed expire time checks, now 2..4 weeks as per RFC. + + * sleuth (check_name): Allow prefix sizes in reverse zones. Replaced the + "all-digit name component" check by "all-digit name". + + * sleuth: Load a configuration file sleuth.conf upon startup. Moved all + the hard-wired parameters there. + + * sleuth: Made severity of all messages configurable. + +2000-10-29 Martin Mares + + * sleuth (resolve): Changed 2181/10.2,3 to 2181/10.2-3 to get the + references right. Thanks to Marcel Telka for a bug report. + +Tue Sep 14 15:03:39 1999 Martin Mares + + * sleuth (try_resolve): Don't treat query send errors as fatal. + +Mon Sep 13 10:04:43 1999 Martin Mares + + * sleuth (check_zone): Added missing ref to RFC 1912/2.7 to + the `wildcard only for MX' message. + (check_name): Don't cry about invalid characters in proper wildcard names. + +Sun Sep 12 21:53:51 1999 Martin Mares + + * check.cgi: When sending mail with secondary name service request, + don't forget to mention the server. + +Tue Jun 8 20:57:33 1999 Martin Mares + + * check.cgi: Implemented $sec_ns_addr_space check. + + * sleuth (check_zone): Commented out the localhost.$domain check + as it's defined only in already obsolete RFC's. + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f093268 --- /dev/null +++ b/Makefile @@ -0,0 +1,36 @@ +# $Id$ +# Makefile for The DNS Sleuth +# (c) 1999--2001 Martin Mares + +CONF_PREFIX=/etc +PREFIX=/usr +VERSION=1.3 + +all: + +clean: + rm -f `find . -name "*~" -o -name "*.[oa]" -o -name "\#*\#" -o -name TAGS -o -name core` + rm -rf dist + +install: all + install -m 644 sleuth.conf $(CONF_PREFIX)/ + install -m 755 sleuth $(PREFIX)/bin + +release: + sed "s/\\(, version \\).*\./\\1$(VERSION)$(SUFFIX)./" README.new + mv README.new README + +REL=sleuth-$(VERSION) +DIST=dist/$(REL) + +dist: clean + mkdir dist + cp -a . $(DIST) + rm -rf `find $(DIST) -name CVS -o -name tmp` $(DIST)/dist + for a in $(DIST)/{sleuth.conf,check.conf} ; do \ + sed 's/^push @INC/#push @INC/' <$$a >$$a.new ; \ + mv $$a.new $$a ; \ + done + cd dist ; tar czvvf /tmp/$(REL).tar.gz $(REL) + +.PHONY: all clean install dist release diff --git a/README b/README new file mode 100644 index 0000000..491e471 --- /dev/null +++ b/README @@ -0,0 +1,169 @@ +This package contains the DNS Sleuth, version 1.3. + +Copyright (c) 1999--2001 Martin Mares + +All files in this package can be freely distributed and used according +to the terms of the GNU General Public License, either version 2 or +(at your opinion) any newer version. The exact text of the license can +be found in file COPYING in any of GNU packages or at FSF Web pages +at URL http://www.gnu.org/copyleft/ + + + Sleuth is a Perl script designed for easy checking of DNS zones +for common errors and also for processing of secondary name service +requests. I wrote it after I've examined at least a dozen of utilities +claiming to do this job and found that all of them are either unable +to discover most zone bugs or too ugly for me to maintain. Sleuth also +lists the corresponding RFC references with most of its error messages, +so that the people upset with their zones being buggy can simply look +up what exactly is going wrong and how to fix it. + + Sleuth requires the Perl DNS module which can be found at +ftp://ftp.cpan.org/pub/CPAN/modules/by-category/05_Networking_Devices_IPC/Net/Net-DNS-0.12.tar.gz. +If you want to install it locally in your home directory, just modify +the @INC path in sleuth.conf. Sleuth has been developed under Perl +5.004_03 and it's probable that bugs in earlier Perl releases may +prevent it from working properly. + + You can download the current version from ftp://atrey.karlin.mff.cuni.cz/pub/local/mj/net/ +or try the online version at http://atrey.karlin.mff.cuni.cz/~mj/sleuth/ . + + Please send me all bug reports and suggestions to . This +will help me with making Sleuth even more useful. + + If you're tired of manually editing all the zone files and syncing the +reverse records by hand, look at NSC -- a suite of M4 scripts for easy +maintenance of DNS zones, you can download it from the same directory +where Sleuth lives, look for "nsc-*.tar.gz". + + The rest of this file tries to provide at least few bits of documentation. + + Have fun + Martin + + +Usage +~~~~~ +To check a zone for consistency, just run "sleuth ". + +To check a zone on specified name server, use "sleuth " +where is the _name_ of the server. If the server itself is not +yet registered, just add its IP address: "sleuth ". + +Also, Sleuth can be used for checks of secondary name service requests +(this includes all of the usual zone checks plus several special ones, +see below for a full list). To turn this mode on, just add two more +parameters: the name of your secondary server and its IP address: +"sleuth ". + +By default, Sleuth lists only resource records defined in the zone being +checked. By specifying a "-v" switch, Sleuth switches to verbose mode +and includes all records it looks at during the checks (e.g., all the +reverse records). + +If you want to check a private zone (i.e., skip all the tests regarding +connection to the worldwide DNS and stop warning about private addresses +occuring), add a "-p" switch. + +You can also switch formatting of output by specifying either "-m" +(plain output -- just lines with their categories, useful for feeding +to an external formatting engine) or "-h" (HTML fragment output, +used by the WWW interface). + + +WWW Interface +~~~~~~~~~~~~~ +This package also includes a simple CGI script which allows Sleuth to be +used interactively from any form-capable Web browser. The CGI interface +(check.cgi) requires the CGI Perl module (standard part of recent Perl +distributions or look at CPAN if you don't have it). + +The script needs some bits of customization, so please look at the +check.conf file and follow the comments. + +The script expects Sleuth and check.conf to be in the same directory +as it's run from. + + +Configuration +~~~~~~~~~~~~~ +You can customize Sleuth by editing the configuration file sleuth.conf +(just follow the comments) which should be placed either in /etc or in +the same directory as the sleuth script itself. + + +Errors checked +~~~~~~~~~~~~~~ +Here is a table of all the checks we do together with their identifiers. +You can set severity of any of the checks (ignore/warning/error/fatal error) +in the configuration file. + +dnserr Fatal DNS error (truncated errors and some other nasties) +reserr Resolver error +selfa Server unable to resolve its own name +badname Malformed domain name +badrn Malformed domain name in reverse zones +zcname Zone is a CNAME +znexist Zone doesn't exist +nonsa Unable to find IP address of the DNS server +pcname DNS record pointing to CNAME +rcname CNAME pointing to CNAME +badrev Invalid reverse mapping +norev Missing reverse mapping +inexrev Inexact reverse mapping (name -> ip -> different names only) +soamail "@ instead of ." and other syntactic errors in SOA zone master address +soammx Missing MX record for zone master address +soammxa Missing A record for that MX record +soaamx A record used instead of MX record +soaorg Missing A record for origin server +recchk The nameserver should be able to answer trivial queries +nolocal No localhost records +badloc Bad localhost records +revloc No reverse record for 127.0.0.1 +unkrevz Unable to find network number in zone name +badrevn Illegal name in reverse zone +badrevr Illegal record type in reverse zone +arev A records in reverse zones are considered bad practice +revcn Illegal CNAME in reverse zone +ptrnoa No A for PTR record +ptrbada Mismatched A for PTR record +outzone Out of zone records +wildac Wildcard A's and CNAME's are strongly deprecated +wild Wildcard records considered bad practice +reccn CNAME recursion +suspcn Suspicious overlapping CNAME +dangcn Dangling CNAME +dangcnr Dangling CNAME in reverse zone +missrev Missing PTR for A +missa Missing A for MX/NS/... destination +obsrec Obsolete records (MD, MF, MB, MG, MR) +supsoa Superfluous SOAs +ptrfwd PTR records in forward zones are considered bad practice +mxpref Invalid preference in MX record +cnclash CNAME together with other records or two CNAME's for same name +twons A zone has to have at least two nameservers +lamer Lame delegations [check mode only] +oodsec Authoritative servers don't agree on domain versions [check mode only] +nosecns Our secondary not listed between NS records [submit mode only] +utoplev Unknown top-level domain [submit mode only] +xtoplev Name of top-level domain used as zone name [submit mode only] +rtoplev Registration of top-level domain attempted [submit mode only] +alknown Already known at our secondary [submit mode only] +snauth Selected nameserver is not zone source [submit mode only] +missns No NS records present [submit mode only] +suspttl Suspicious TTL +suspmtl Suspicious minttl in SOA +suspexp Suspicious expire in SOA +wks WKS record is obsolete +ornotns Origin server not listed in domain's NS records +unxtype Unexpected record in reply packet +axfer Zone transfer failed +alldig All-digit names are not allowed +noserv No name server available for checking +diffns Different name servers report different set of NS records +duprec Duplicate record in zone +srvnam Invalid name of SRV record +srvpar Invalid parameters of SRV record +srvdest Destination of SRV has no A +iapname IP address found instead of name +needaa Answer is not authoritative diff --git a/THANKS b/THANKS new file mode 100644 index 0000000..1277dcd --- /dev/null +++ b/THANKS @@ -0,0 +1,14 @@ +Thanks to people who have contributed their bug reports, ideas and patches +and helped me make the DNS Sleuth what it is now. + +Ludek Coufal +Lourdes Jones +Pavel Kankovsky +Jan Kasprzak +Vladimir Kotal +Michael Mraka +Petr Nachtmann +Vojtech Pavlik +Jakub Skopal +Petr Soucek +Marcel Telka diff --git a/TODO b/TODO new file mode 100644 index 0000000..0f381a8 --- /dev/null +++ b/TODO @@ -0,0 +1,8 @@ +Unsorted ideas: +~~~~~~~~~~~~~~~ +- check whether different nameservers are in different networks +- check both TCP and UDP queries +- SOA admin address: wildcard MX record? +- IPv6 records and reversals +- detailed host/mail/domain name checks +- cgi: memory limits? diff --git a/check.cgi b/check.cgi new file mode 100755 index 0000000..c415e1c --- /dev/null +++ b/check.cgi @@ -0,0 +1,192 @@ +#!/usr/bin/perl +# +# A Simple WWW interface for Sleuth domain checker +# (c) 1999--2001 Martin Mares +# + +# Load configuration file and all modules we need + +BEGIN { require "check.conf"; } + +use CGI; +use IO::Handle; + +$query = new CGI; +print $query->header; + +$domain = $query->param('domain'); +$server = $query->param('server'); +$serverip = $query->param('serverip'); +$verbose = $query->param('verbose'); +$secondary = $query->param('secondary'); +$submitter = $query->param('submitter'); +$submit = "Submit"; + +$stylesheet = <start_html( -title=>"DNS Sleuth", + -author=>"$admin_email", + -dtd=>'-//W3C//DTD HTML 4.01//EN', + -style=>{-code=>$stylesheet} + ); + +print < +

The DNS Sleuth

+

An online tool for checking of DNS zones + +

+

Sleuth version $version ($rev_date) +

+
+

+Written by +$author_ref +<$author_email> +

+EOF +; + +if ($secondary_ns eq "") { + $secondary = ''; +} +$msg = "New query"; +if ($domain eq "") { + print <This is an online version of Sleuth -- a detector +of common errors in DNS zones. + +

To check a zone, just enter its name in the form below. You can explictly +specify a name server to be asked for zone data and if its name isn't available +from the public DNS, then also its IP address. If you select the verbose mode, +you will also get a full dump of the zone at no extra cost. +EOF +; + if ($secondary_ns ne "") { + print <If you want to submit a request for secondary DNS service, fill in all +form fields and check the "request secondary" box. The secondary server will +run on $secondary_ns, so don't forget to list it as an +authoritative server in your zone file. +EOF +; + } + print <If you want to run Sleuth on your own machine, just download +the latest version from our FTP server. Sleuth is free software, you can freely use and distribute it according +to the GNU General Public License. +

If this page looks a bit fruitless, but otherwise displayed correctly, +it's probably because your browser doesn't support the Cascading +Style Sheets which are The Right Way to define presentation of HTML documents. +In case this page looks a bit weird, your browser probably tries +to support the style sheets, but does it wrongly (hello, Netscape 4). +EOF +; +} elsif ($secondary ne "" && $serverip eq "") { + $msg = "Missing server name"; +} elsif ($secondary ne "" && $submitter eq "" && $need_submitter) { + $msg = "Please fill in who is submitting the zone"; +} elsif ($server !~ /^[0-9A-Za-z.-]*$/ || + $serverip !~ /^(\d+\.\d+\.\d+\.\d+)?$/ || + $domain !~ /^[0-9A-Za-z.-]*$/ || + $submitter !~ /^[0-9A-Za-z.\240-\376 -]*$/) { + $msg = "Incorrect parameter syntax"; +} elsif ($secondary ne "" && defined $sec_ns_addr_space && + unpack("H8",pack("c4",split(/\./,$serverip))) !~ $sec_ns_addr_space) { + $msg = "Name server address out of permitted range"; +} else { + print "

Check results for $domain

\n"; + @c = "./sleuth"; + if ($verbose ne "") { push @c, "-v"; } + push @c, "-h", $domain, $server; + if ($serverip ne "") { push @c, $serverip; } + if ($secondary ne "") { push @c, $secondary_ns, $secondary_ns_ip; } + STDOUT->flush(); + $rc = system @c; + if ($secondary ne "") { + if ($rc / 256) { + print "

Request for secondary DNS service rejected because of errors in the zone.\n"; + print "

If you have any questions, please ask $admin_ref.\n"; + } else { + print "

Forwarding your secondary DNS service request to $admin_ref\n"; + print "who will notify you by e-mail sent to the hostmaster address given in the\n"; + print "SOA record when the secondary server will be configured.\n"; + shift @c; + while ($c[0] =~ /^-[vh]$/) { shift @c; } + $ru = $query->remote_user; + ($rh = $query->remote_host) =~ s/[^A-Za-z0-9. \240-\376-]/?/g; + # This might look insecure, but we know all the names have been correctly + # validated by the first sleuth run. + `( echo "Domain: $domain" ; + echo "Server: $server [$serverip]" ; + echo "User: $ru" ; + echo "From: $submitter ($rh)" ; + echo ; + ./sleuth @c + ) | mail -s \"New zone\" $secondaries_to`; + $? && print "

Forwarding failed (rc=$?)\n"; + } + } + print "


\n"; +} + +print $query->startform(-method=>'GET'); +print "\ +

\n\ +\n"; +print "\n"; +print "\n"; +print "\n"; +print "\n"; +if ($secondary_ns ne "") { + print "\n"; + print "\n"; +} +print "\n"; +print "
$msg
Domain:", $query->textfield('domain'), "Verbose output:", $query->checkbox(-name=>'verbose',value=>'ON',-label=>''), "
Server name:", $query->textfield('server'), "Server IP:", $query->textfield('serverip'), "
Request secondary:", $query->checkbox(-name=>'secondary',value=>'ON',-label=>''), "Your name:", $query->textfield('submitter'), "
", $query->submit('action', 'Submit'), "
\n"; +print $query->endform; + +print "

Please send your bug reports and suggestions to $author_ref.

\n"; +print $query->end_html; +print "\n"; diff --git a/check.conf b/check.conf new file mode 100644 index 0000000..5ca653c --- /dev/null +++ b/check.conf @@ -0,0 +1,26 @@ +# +# Configuration for check.cgi +# (c) 1999--2001 Martin Mares +# +# This file is just a piece of Perl code imported during +# initialization of the check.cgi script, so you can do here +# any Perl magic you wish here. +# + +# Path to the CGI module +#push @INC, "/home/mj/perl/lib/site_perl"; + +$version = "1.3"; +$rev_date = "16-06-01"; +$admin_email = "mj\@ucw.cz"; +$admin_ref = "Martin Mares"; +$author_email = "mj\@ucw.cz"; +$author_ref = "Martin Mares"; + +#$secondary_ns = "server1.gts.cz"; # Comment out if you wish to disable the secondary DNS request fields +#$secondary_ns_ip = "194.213.32.2"; +#$secondaries_to = "mj\@ucw.cz"; # Where to send secondary zone requests +#$need_submitter = 1; # The Submitter field is mandatory +#$sec_ns_addr_space = /^$/; # For secondary requests, hexadecimal IP address of NS must match this. + +1; # To make file loading successful :) diff --git a/sleuth b/sleuth new file mode 100755 index 0000000..3c89bad --- /dev/null +++ b/sleuth @@ -0,0 +1,677 @@ +#!/usr/bin/perl -w +# +# Sleuth -- A Simple DNS Checking Tool +# +# (c) 1999--2001 Martin Mares +# + +# Load configuration file and all modules we need + +BEGIN { + if (-f "/etc/sleuth.conf") { + require "/etc/sleuth.conf"; + } else { + require __FILE__ . ".conf"; + } +} + +use Getopt::Std; +use Net::DNS::Resolver; + +# Parse arguments + +getopts('vmhp', \%opts) && (@ARGV >= 1 && @ARGV <= 3 || @ARGV == 5) || do { + print < [ [ [ ]]] + +-h Produce HTML output +-m Produce machine-readable output +-p Private network mode (avoid public accessibility checks) +-v Be verbose +EOF +; + exit 1; +}; +$domain = norm_name($ARGV[0]); +$mode_submit = @ARGV == 5; +$check_ns = defined($ARGV[1]) ? norm_name($ARGV[1]) : ""; +$check_ns_ip = defined($ARGV[2]) ? $ARGV[2] : ""; +$our_name = defined($ARGV[3]) ? norm_name($ARGV[3]) : ""; +$our_ip = defined($ARGV[4]) ? $ARGV[4] : ""; + +$verbose = $opts{"v"}; +$private = $opts{"p"}; +if ($opts{"m"}) { $output = \&plain_output; } +elsif ($opts{"h"}) { $output = \&html_output; } +else { $output = \&fancy_output; } + +# Initialize reliable resolver using our local nameserver. + +$rres = new Net::DNS::Resolver; +$rres->defnames(0); +$rres->dnsrch(0); +$rres->debug(0); +# FIXME: Net::DNS doesn't implement persistent vc's yet +#$rres->usevc(1); +#$rres->stayopen(1); + +# And do the checks... + +info("Starting zone checks for $domain"); +$err_cnt = 0; +$warn_cnt = 0; +if ($mode_submit) { + check_zone_name(); + check_submit(); + check_ns_sanity(); + check_zone() || msg("noserv", "No zone data available, giving up"); +} else { + check_zone_name(); + check_zone_basics(); + $global_okay = 0; + foreach my $nsvr (@check_servers) { + $nsvr =~ /(.*) = (.*)/; + $check_ns = $1; + $check_ns_ip = $2; + info("Decided to use $check_ns ($check_ns_ip) for zone check"); + init_resolver($check_ns_ip); + check_ns_sanity(); + if (check_zone()) { + $global_okay = 1; + last; + } + } + $global_okay || msg("noserv", "No name server available for checking"); +} +info("Summary: $err_cnt errors, $warn_cnt warnings"); + +exit ($err_cnt > 0); + +# Output of messages + +sub plain_output { + my $type = shift @_; + my $msg = shift @_; + my $ref = shift @_; + $ref = (defined $ref) ? " [RFC$ref]" : ""; + print "$type $msg$ref\n"; +} + +sub fancy_output { + my $type = shift @_; + my $msg = shift @_; + my $ref = shift @_; + my $mmsg; + my %msg_types = %{{ 'W' => '### Warning: ', + 'E' => '### Error: ', + 'F' => '### Fatal error: ', + '>' => ' ', + '*' => ' -> ', + '.' => '' + }}; + $mmsg = $msg_types{$type}; + $ref = (defined $ref) ? " [RFC$ref]" : ""; + print "$mmsg$msg$ref\n"; +} + +sub html_output { + my $type = shift @_; + my $msg = shift @_; + my $ref = shift @_; + if ($type =~ /[>*]/) { + if (!$is_pre) { print "
"; $is_pre=1; }
+		print "    ", ($type eq ">") ? "" : "-> ", $msg;
+	} else {
+		if (!defined $is_pre) { print "

"; $is_pre=0; } + elsif ($is_pre) { print "

"; $is_pre=0; } + else { print "
"; } + if ($type =~ /[WEF]/) { + my $map = {'W'=>'Warning', 'E'=>'Error', 'F'=>'Fatal error'}; + print "### ", ${$map}{$type}, ": $msg", ""; + } elsif ($type eq "." && $msg =~ /^Summary: /) { + if ($msg !~ / 0 errors,/) { $msg =~ s/ (\d+) errors,/ $1 errors,<\/em>/; } + if ($msg !~ / 0 warnings/) { $msg =~ s/ (\d+) warnings/ $1 warnings<\/em>/; } + print $msg; + } else { print $msg; } + if (defined $ref) { + my $comma = 0; + print "  [see"; + foreach my $z (split(/,\s*/, $ref)) { + my ($rfc, $url); + $comma++ && print ","; + if ($z =~ /(\d+)\/(.*)/) { + $rfc = "$1:$2"; + $url = eval $rfc_sec_url; + } elsif ($z =~ /(\d+)/) { + $rfc = "$1"; + $url = eval $rfc_url; + } else { die "Bad RFC reference"; } + print " RFC$rfc"; + } + print " for details]"; + } + } + print "\n"; +} + +sub msg { + my ($id, $msg, $ref) = @_; + defined $sev{$id} or die "Internal error: unknown message code <$id>"; + my $type = $sev{$id}; + return if $type eq ""; + if (!$verbose) { + if ($type =~ /[.>]/) { @msg_buffer = (); } + elsif ($type =~ /[EWF]/ && @msg_buffer) { + foreach my $m (@msg_buffer) { &{$output}('*', $m); } + @msg_buffer = (); + } elsif ($type eq '*') { push @msg_buffer, $msg; return; } + } + &{$output}($type, $msg, $ref); + if ($type eq "E") { $err_cnt++; } + elsif ($type eq "W") { $warn_cnt++; } + elsif ($type eq "F") { exit 1; } +} + +sub info { msg('.', shift @_); } +sub rr_echo { my $rr=shift @_; msg('*', $rr->string); } + +# Our interface to the resolver + +sub try_resolve { + my $rver = shift @_; + my $name = shift @_; + my $type = shift @_; + my $need_aa = shift @_; + my $q = $rver->send($name, $type, "IN") or do { + msg("reserr", $res->errorstring); + return undef; + }; + my $hdr = $q->header; + $hdr->tc && msg("dnserr", "Truncated response received"); + my $rc = $hdr->rcode; + $rc eq "NXDOMAIN" and return undef; + $rc eq "NOERROR" or do { msg("reserr", "Unable to resolve $name: $rc"); return undef; }; + $hdr->ancount || return undef; + !$need_aa || $hdr->aa || msg("needaa", "Answer is not authoritative"); + return $q; +} + +sub resolve { + my $name = shift @_; + my $type = shift @_; + my $allow_cnames = shift @_; + my $check_rev = shift @_; + my $need_aa = shift @_; + my @answer; + check_name($name) || return (); + if ($cache{$name}{$type}) { + @answer = @{$cache{$name}{$type}}; + } else { + my $q = try_resolve($res, $name, $type, $need_aa); + $q || return (); + @answer = $q->answer; + $cache{$name}{$type} = \@answer; + } + my @result = (); + my $cname_cnt = 0; + foreach my $r (@answer) { + rr_echo($r); + $r->class ne "IN" and next; + if ($r->type eq "A") { + # If it's an A record, automatically check it maps back. + $check_rev && check_reverse($r->name, $r->address, ""); + } + if ($r->type eq "CNAME") { + if (!$allow_cnames) { msg("pcname", "DNS records must not point to CNAMEs", "1034/3.6, 1912/2.4, 2181/10.2-3"); } + if ($cname_cnt) { msg("rcname", "CNAMEs must not point to CNAMEs", "1034/3.6, 1912/2.4, 2181/10.2-3"); } + $cname_cnt++; + } + if ($r->type eq $type || $type eq "ANY") { + # We shouldn't check minimum TTL here as we might have got a cached value + ($r->ttl > 4*7*86400) && msg("suspttl", "Suspicious TTL value"); + push @result, $r; + } elsif ($r->type ne "CNAME") { + msg("unxtype", "Expected $type, got " . $r->type); + } + } + return @result; +} + +# Normalization and comparison of host names and IP addresses + +sub same_ipa { + my $x = shift @_; + my $y = shift @_; + return $x eq $y; +} + +sub norm_name { + my $n = shift @_; + $n =~ s/\.$//; + $n =~ tr[A-Z][a-z]; + return $n; +} + +sub same_name { + my $x = shift @_; + my $y = shift @_; + return norm_name($x) eq norm_name($y); +} + +# Checks of reverse mapping + +sub reverse_name { + my $addr = shift @_; + $addr =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/ or fatal("Unable to parse IP address $addr"); + return "$4.$3.$2.$1.in-addr.arpa."; +} + +sub check_reverse { + my $name = shift @_; + my $addr = shift @_; + my $allow_domain = shift @_; + my $rn = reverse_name($addr); + my $maps_back = ""; + my $found_exact = 0; + my $warned = 0; + $did_reverse_check{$addr} && return; + $did_reverse_check{$addr} = 1; + ($addr =~ /^(192\.168|10|172\.(1[6-9]|2\d|3[01]))\./) && !$private && + msg("privadr", "Private addresses shouldn't occur in public zones", "1918"); + foreach my $q (resolve("$rn", "PTR", 1, 0)) { + my $dname = $q->ptrdname; + if (same_name($dname, $name)) { $found_exact++; } + else { + my $matched = 0; + foreach my $a (resolve("$dname", "A", 1, 0)) { + same_ipa($a->address, $addr) && ($matched = 1); + } + if (!$matched) { + $warned = 1; + msg("badrev", "$name maps to $dname which doesn't point back", "1912/2.1"); + } + $maps_back = $dname; + } + } + if (!$found_exact) { + if ($maps_back eq "") { msg("norev", "$name ($addr) has no reverse record", "1912/2.1"); } + elsif ($name ne $allow_domain && !$warned) { + msg("inexrev", "$addr for $name points back to $maps_back", "1912/2.1"); + } + } +} + +# Check of e-mail address + +sub check_email { + my $e = shift @_; + $e =~ /@/ && do { msg("soamail", "'\@' in e-mail addresses should be encoded as '.'", "1912/2.2"); return; }; + $e =~ /^(([^.\\]|\\.)+)\.(.*)$/ || do { msg("soamail", "Invalid e-mail address syntax"); return; }; + my $user = $1; + my $host = norm_name($3); + $user =~ s/\\(.)/$1/g; + $e = "$user\@$host"; + info("Hostmaster e-mail address is $e"); + if (my @mx = resolve($host, "MX", 1, 0)) { + foreach my $r (@mx) { + resolve($r->exchange, "A", 0, 1) or msg("soammxa", "No A record for MX " . $r->exchange); + } + } elsif (resolve($host, "A", 1, 0)) { + msg("soaamx", "MX records should be used for mail routing"); + } else { + msg("soammx", "No MX record for $host"); + } +} + +# Check of name syntax + +sub check_name { + my $n = shift @_; + $n =~ s/\.$//; + if ($n !~ /[^0-9.]/) { + if ($n =~ /^(\d+\.){3}\d+$/) { msg("ipaname", "IP address found instead of name", "1912/2.1"); } + else { msg("alldig", "All-digit names are not allowed", "1912/2.1"); } + return 0; + } + if ($n =~ /^(.*)\.in-addr\.arpa$/i) { + my $m = $1; + if ($m !~ /^(\d+-\d+|\d+)(\/\d+)?(\.(\d+-\d+|\d+)(\/\d+)?)*$/) { + msg("badrn", "Invalid name of reverse domain $n", "1035/3.5"); return 0; + } elsif ($m =~ /(^|\.|-|\/)0[0-9]/) { + msg("badrn", "Reverse names should not contain leading zeros", "1035/3.5"); return 0; + } + } + $n =~ s/^\*\.//; + foreach my $q (split(/\./, $n)) { + if ($q eq "") { + msg("badname", "Name $n has empty components", "1912/2.1"); return 0; + } elsif ($q !~ /^[0-9a-zA-Z_-]*$/) { + msg("badname", "Name $n contains invalid characters", "1912/2.1"); return 0; + } + } + return 1; +} + +# Generic checks of nameserver configuration + +sub check_ns_sanity { + # Check whether the nameserver is able to resolve its own address forth and back. + + info("Checking whether $check_ns knows an A record for its own name"); + my $ns_a = 0; + foreach $r (resolve($check_ns, "A", 0, 0)) { + if (same_ipa($check_ns_ip, $r->address)) { $ns_a++; } + } + $ns_a || msg("selfa", "no matching A record found"); + + info("Checking whether $check_ns is able to reverse-map its own IP address"); + check_reverse($check_ns, $check_ns_ip, ""); + + # General nameserver functionality checks + + if (!$private) { + info("Checking connectivity with other nameservers"); + foreach $name (@test_hosts) { + resolve($name, "A", 1, 1) or + msg("recchk", "$check_ns is unable to resolve $name (maybe it's non-recursive)"); + } + } + + info("Checking mapping of localhost"); + $res->recurse(0); + if (@lh = resolve("localhost", "A", 1, 0, 1)) { + (@lh != 1 || !same_ipa($lh[0]->address, "127.0.0.1")) && + msg("badloc", "Invalid resource records for localhost at $check_ns", "1912/4.1"); + } else { msg("nolocal", "$check_ns is unable to resolve localhost", "1912/4.1"); } + resolve("1.0.0.127.in-addr.arpa", "PTR", 1, 0, 1) or msg("revloc", "Reverse mapping of 127.0.0.1 at $check_ns doesn't work", "1912/4.1"); + $res->recurse(1); +} + +# Zone name checks + +sub check_zone_name { + info("Checking zone name"); + check_name($domain); + if ($domain =~ /^(.*)\.in-addr\.arpa$/) { + my $rev = $1; + if ($rev =~ /(^|\.)(\d+(\.\d+){2})$/) { + $rev_net = join('.', reverse split (/\./, $2)) . '.'; + info("Switched to reverse zone check mode for network $rev_net"); + } else { + msg("unkrevz", "Switched to reverse mode, but unable to find network number in zone name", "1035/3.5"); + } + $reverse = 1; + } +} + +# Checks done for zone submission + +sub check_submit { + # Test for bogus and forbidden names + + ($domain =~ /(^|\.)([^.]+)\.([^.]+)$/) || msg("rtoplev", "Registration of top-level domains not supported"); + $l2 = $2; + $l1 = $3; + try_resolve($rres, $l1, "SOA") || msg("utoplev", "Top level domain $l1 doesn't exist"); + if (length($l2) <= 4 && ($q = try_resolve($rres, $l2, "SOA"))) { + rr_echo($q->answer); + msg("xtoplev", "Second-level domains must not use names of top-level domains"); + } + + # Test whether our NS is not already authoritative. + + info("Checking for zone duplicity for $domain"); + init_resolver($our_ip); + $res->recurse(0); + $q = try_resolve($res, $domain, "SOA"); + ($q && $q->header->aa) && msg("alknown", "$domain already known at $our_name"); + + # Test whether the NS is authoritative for the zone. + + init_resolver($check_ns_ip); + info("Checking for authoritative data for $domain"); + $res->recurse(0); + $q = try_resolve($res, $domain, "SOA"); + $q || msg("snauth", "SOA record for $domain not found"); + $q->header->aa || msg("snauth", "$check_ns is not authoritative for $domain"); + $res->recurse(1); + + # Check number of name servers and whether we are one of them. + + info("Checking list of nameservers"); + @q = resolve($domain, "NS", 0, 1) or msg("missns", "No NS records for $domain found"); + @q >= 2 || msg("twons", "Each domain should have at least 2 nameservers", "1912/2.8"); + if (defined($our_name)) { + $found_us = 0; + foreach $r (@q) { + same_name($r->nsdname, $our_name) && ($found_us = 1); + } + $found_us || msg("nosecns", "$our_name is not listed in NS records of $domain"); + } +} + +# Zone transfer and check + +sub check_zone { + +info("Fetching zone data for $domain"); +if (!(@zone = $res->axfr($domain))) { + msg("axfer", "Zone transfer failed"); + return 0; +} + +info("Parsing zone data"); +$rcnt=0; +foreach $r (@zone) { + $records{norm_name($r->name)}{$rcnt++} = $r; +} + +info("Checking consistency of zone records"); +$seen_localhost = 0; + +foreach $name (sort { ($a eq $domain) ? -1 : ($b eq $domain) ? 1 : ($a cmp $b) } keys %records) { + $seen_cname = 0; + $seen_other = 0; + foreach $z (keys %{$records{$name}}) { + $r = $records{$name}{$z}; + my $txt = $r->string; + msg(">", $txt); + defined $seen{$txt} && msg("duprec", "Duplicate record"); + $seen{$txt} = 1; + check_name($name); + ($r->ttl < 3600 || $r->ttl > 4*7*86400) && msg("suspttl", "Suspicious TTL value"); + $t = $r->type; + $name =~ /(^|\.)$domain$/ || msg("outzone", "Out-of-zone record"); + if ($name =~ /\*/) { + if ($t eq "SRV") { + # Wildcard SRV's are a useful thing + } elsif ($t eq "A" || $t eq "CNAME") { + msg("wildac", "Wildcard A and CNAME records are likely to be very confusing", "1912/2.7"); + } else { + msg("wild", "Wildcard names are generally considered bad practice", "1912/2.7"); + } + } + if ($reverse) { + ($name =~ /^(0|[1-9]\d*)\.$domain$/ && ($num = $1) < 256) || + ($name eq $domain && $t ne "CNAME" && $t ne "PTR") || + msg("badrevn", "Reverse zones should contain only numeric names"); + if ($t =~ /^(MX|WKS)$/) { + msg("badrevr", "Illegal record in reverse zone"); + } elsif ($t eq "A") { + msg("arev", "A records in reverse zones are valid, but considered bad practice", "1912/2.3"); + } + } else { + if ($t eq "PTR") { + msg("ptrfwd", "PTR records in forward zones are valid, but considered bad practice"); + } + } + if ($t eq "CNAME") { + $seen_cname++; + $d = norm_name($r->cname); + # a.b.c -> a.b.c is wrong + if (same_name($d, $name)) { msg("reccn", "Recursive CNAME", "1034/3.6"); } + else { + # a.b.c -> x.a.b.c and a.b.c -> b.c are probably wrong as well, but not forbidden + if ($name =~ /(^|\.)$d/i || $d =~ /(^|\.)$name/) { + msg("suspcn", "Possibly incorrect overlapping CNAME"); + } + if (!resolve($d, "ANY", 0, 1)) { + if ($reverse) { + msg("dangcnr", "Unable to resolve CNAME destination (probably due to classless delegation)"); + } else { msg("dangcn", "Unable to resolve CNAME destination"); } + } + } + } else { $seen_other++; } + if (same_name($name, "localhost.$domain")) { + if ($t eq "A" && same_ipa($r->address, "127.0.0.1")) { $seen_localhost++; next; } + else { msg("badloc", "Invalid localhost record"); } + } + if ($t eq "A") { + check_reverse($name, $r->address, $domain); + } elsif ($t eq "NS") { + $dest = $r->nsdname; + resolve($dest, "A", 0, 1) || msg("missa", "Nameserver $dest doesn't have any valid A records"); + } elsif ($t =~ /^(MD|MF|MB|MG|MR)$/) { + msg("obsrec", "MD/MF/MB/MG/MR records are obsolete and should not be used"); + } elsif ($t eq "SOA") { + (same_name($name, $domain)) || do { + msg("supsoa", "Superfluous SOA record"); + next; + }; + resolve($r->mname, "A", 0, 1) || msg("soaorg", "No A record for zone origin"); + check_email($r->rname); + ($r->expire < 2*7*86400 || $r->expire > 4*7*86400) && + msg("suspexp", "Expire time should be between 2 and 4 weeks", "1912/2.2"); + ($r->minimum < 3600) && msg("suspmtl", "Suspicious minimum TTL", "2308/4"); + } elsif ($t eq "WKS") { + msg("wks", "WKS record is obsolete and should not be used", "1912/2.6.1"); + } elsif ($t eq "PTR") { + if (@dd = resolve($r->ptrdname, "A", 0, 0)) { + if (defined $rev_net) { + $found = 0; + foreach $rr (@dd) { + (same_ipa($rr->address, $rev_net . $num)) && ($found=1); + } + $found || msg("ptrbada", "No corresponding A record found", "1912/2.4"); + } + } else { msg("ptrnoa", "PTR doesn't point to an A record", "1912/2.4"); } + } elsif ($t eq "MX") { + ($r->preference >= 0 && $r->preference < 65536) || msg("mxpref", "Invalid MX preference", "1035/3.3.9"); + $dest = $r->exchange; + resolve($dest, "A", 0, 1) || msg("missa", "Mail exchanger $dest doesn't have any valid A records", "1035/3.3.9"); + } elsif ($t eq "SRV") { + ($name =~ /^(\*|_[0-9a-zA-Z]+)\.(\*|_[a-zA-Z]+)\./) || msg("srvnam", "Invalid service name", "2782"); + ($r->priority >= 0 && $r->priority < 65536) || msg("srvpar", "Invalid SRV preference", "2782"); + ($r->weight >= 0 && $r->weight < 65536) || msg("srvpar", "Invalid SRV weight", "2872"); + ($r->port >= 0 && $r->port < 65536) || msg("srvpar", "Invalid SRV port number", "2872"); + $r->target eq "" || $r->target eq "." || resolve($r->target, "A", 0, 1) || + msg("srvdest", "Service provider has no valid A record"); + } + } + if ($seen_cname > 1) { + msg("cnclash", "Multiple CNAMEs for one name", "1912/2.4"); + } elsif ($seen_cname && $seen_other) { + msg("cnclash", "CNAME is not allowed to coexist with any other data", "1912/2.4"); + } +} +return 1; +} + +# Initialize resolver library, but point it to the nameserver given + +sub init_resolver { + my $name = shift @_; + $res = new Net::DNS::Resolver; + $res->nameservers($name); + $res->recurse(1); + $res->defnames(0); + $res->dnsrch(0); + $res->debug(0); + # FIXME: Net::DNS doesn't implement persistent vc's yet + #$res->usevc(1); + #$res->stayopen(1); +} + +# Basic zone checks -- existence, matching SOA versions and lame delegations +# returns @check_servers + +sub check_zone_basics { + my $prefer_origin; + + # In case check_ns is given, use it for initial checks, else use our local nameserver + if ($check_ns ne "") { + if ($check_ns_ip eq "") { + info("Resolving name-server address"); + $res = $rres; + my @ips = resolve($check_ns, "A", 0, 0) or msg("nonsa", "$check_ns has no A record"); + $check_ns_ip = $ips[0]->address; + } + $prefer_origin = $check_ns; + @check_servers = ( "$check_ns = $check_ns_ip" ); + init_resolver($check_ns_ip); + $rres = $res; # This one will be the reference + } else { + $res = $rres; + @check_servers = (); + } + + info("Checking existence of zone"); + resolve($domain, "CNAME", 1, 0) && msg("zcname", "$domain is a CNAME"); + my @soa = resolve($domain, "SOA", 0, 0) or msg("znexist", "$domain doesn't exist"); + my $real_origin = norm_name($soa[0]->mname); + defined $prefer_origin || ($prefer_origin = $real_origin); + + info("Checking NS records"); + my @ns = resolve($domain, "NS", 0, 0) or msg("missns", "$domain has no NS records"); + (@ns >= 2) || msg("twons", "Each domain should have at least 2 nameservers", "1912/2.8"); + @ns = map { norm_name($_->nsdname) } @ns; + my $nsnames = join(':', sort { $a cmp $b } @ns); + my %nshash; + foreach $r (@ns) { $nshash{$r} = 1; } + if (!defined $nshash{$real_origin}) { msg("ornotns", "Origin server $real_origin not listed in NS records"); } + delete $nshash{$prefer_origin}; + @ns = keys %nshash; + unshift @ns, $prefer_origin; + + info("Checking nameserver authority and synchronization"); + my $psoa; + foreach $n (@ns) { + my @nips; + $res = $rres; + if (!(@nips = resolve($n, "A", 0, 1))) { + msg("missa", "Nameserver $n doesn't have any valid A records"); + next; + } + my $nip = $nips[0]->address; + info("Probing name server $n ($nip)"); + init_resolver($nip); + $res->recurse(0); + my $q = try_resolve($res, $domain, "SOA"); + $res->recurse(1); + $q && $q->header->aa || do { msg("lamer", "Lame delegation of $domain to $n", "1912/2.8"); next; }; + my @ss = resolve($domain, "SOA", 0, 0); + if (!@ss) { msg("lamer", "Lame delegation of $domain to $n", "1912/2.8"); next; } + my $ss = $ss[0]; + + if ($check_ns eq "") { push @check_servers, "$n = $nip"; } + if (defined $psoa) { + my $delta = $psoa->serial - $ss->serial; + ($delta >= 0x80000000) && ($delta -= 0x80000000); + ($delta <= -0x80000000) && ($delta += 0x80000000); + if ($delta > 0) { msg("oodsec", "$n has out of date data for $domain"); } + elsif ($delta < 0) { msg("oodsec", "$n has newer data for $domain than zone origin"); } + if ($psoa->mname ne $ss->mname || + $psoa->rname ne $ss->rname || + $psoa->refresh != $ss->refresh || + $psoa->retry != $ss->retry || + $psoa->expire != $ss->expire || + $psoa->minimum != $ss->minimum) { + msg("oodsoa", "$n lists different SOA parameters than zone origin"); + } + } else { $psoa = $ss; } + + my $nsb = join(':', sort { $a cmp $b } map { $_->nsdname } resolve($domain, "NS", 0, 0)); + same_name($nsnames,$nsb) || msg("diffns", "Different set of NS records reported ($nsb)"); + } + # info("Continuing zone checks on " . join("/", @check_servers)); +} diff --git a/sleuth.conf b/sleuth.conf new file mode 100644 index 0000000..0ccfe29 --- /dev/null +++ b/sleuth.conf @@ -0,0 +1,102 @@ +# +# Configuration for the DNS Sleuth +# (c) 1999--2001 Martin Mares +# +# This file is just a piece of Perl code imported during +# initialization of the Sleuth script, so you can do here +# any Perl magic you wish. +# + +# Path to the DNS module +#push @INC, "/home/mj/perl/lib/site_perl"; + +# Hosts we check recursive resolving on +@test_hosts = ( 'www.ucw.cz', 'atrey.karlin.mff.cuni.cz', 'metalab.unc.edu' ); + +# Assign categories to all error messages: +# '0' ignore +# '.' informational message +# 'W' warning +# 'E' error +# 'F' fatal error +# Consult the README for a list of all the check names. +%sev = %{{ + 'selfa' => 'E', + 'badname' => 'E', + 'badrn' => 'E', + 'xtoplev' => 'F', + 'alknown' => 'F', + 'snauth' => 'F', + 'axfer' => 'E', + 'twons' => 'E', + 'reserr' => 'E', + 'pcname' => 'E', + 'rcname' => 'E', + 'badrev' => 'E', + 'norev' => 'E', + 'inexrev' => 'W', + 'soamail' => 'E', + 'soammx' => 'E', + 'soaamx' => 'W', + 'soammxa' => 'E', + 'soaorg' => 'E', + 'recchk' => 'W', + 'nolocal' => 'W', + 'badloc' => 'E', + 'revloc' => 'W', + 'nosecns' => 'E', + 'badrevn' => 'E', + 'badrevr' => 'E', + 'arev' => 'W', + 'outzone' => 'E', + 'wildac' => 'W', + 'wild' => 'W', + 'revcn' => 'E', + 'ptrnoa' => 'E', + 'ptrbada' => 'E', + 'reccn' => 'E', + 'suspcn' => 'W', + 'dangcn' => 'E', + 'dangcnr' => 'W', + 'missa' => 'E', + 'obsrec' => 'E', + 'supsoa' => 'E', + 'ptrfwd' => 'W', + 'mxpref' => 'E', + 'cnclash' => 'E', + 'lamer' => 'E', + 'oodsec' => 'E', + 'alldig' => 'E', + 'privadr' => 'W', + 'suspttl' => 'W', + 'unxtype' => 'W', + 'suspexp' => 'W', + 'suspmtl' => 'W', + 'wks' => 'W', + 'ornotns' => 'W', + 'diffns' => 'E', + 'duprec' => 'E', + 'unkrevz' => 'E', + 'srvnam' => 'E', + 'srvpar' => 'E', + 'srvdest' => 'E', + 'ipaname' => 'E', + 'needaa' => 'E', + 'zcname' => 'F', # These have to be fatal + 'znexist' => 'F', + 'nonsa' => 'F', + 'missns' => 'F', + 'dnserr' => 'F', + 'utoplev' => 'F', + 'rtoplev' => 'F', + 'noserv' => 'F', + '*' => '*', # Internal use only, don't modify + '>' => '>', + '.' => '.' +}}; + +# URL for RFC references and RFC section references +$rfc_url = '"http://www.cis.ohio-state.edu/cgi-bin/rfc/rfc$1.html"'; +$rfc_sec_url = '"http://www.cis.ohio-state.edu/cgi-bin/rfc/rfc$1.html#sec-$2"'; + +1; # To make file loading successful :) -- 1.8.3.1