Developer guide

From DNSdoctor

Table of contents

Internal design

Localisation

  <check name="soa_ns_cname">
    <name>SOA master is not an alias</name>
    <success>The SOA master is not an alias</success>
    <failure>SOA master is not allowed to point to a CNAME alias</failure>
    <explanation sameas="shortcut:ns_cname"/>
    <details><para>The master (<var name="master"/>) is a CNAME alias
      to <var name="alias"/>.</para></details>
  </check>
  <shortcut>
    <explanation name="email_sntx">
      <src type="ref" from="rfc" fid="rfc1034#p9">
        <title>IETF RFC1034 (p.9), RFC1912 (p.3)</title>
        <para>Email addresses are converted by using the following rule:
          local-part@mail-domain  ==>  local-part.mail-domain 
          if local-part contains a dot in should be backslashed 
          (for 'bind').</para>
      </src>
    </explanation>
  </shortcut>
   
  <check name="soa_contact_sntx_at">
    <name>misused '@' characters in SOA contact name</name>
    <success>No misuse of '@' character in SOA contact name</success>
    <failure>The contact name contains the character '@'</failure>
    <explanation sameas="shortcut:email_sntx"/>
    <details/>
  </check>


This is the DTD used to describe the message catalog:

 <!ELEMENT msgcat  ((shortcut|check|test)*|(section|tag)*)>
  
 <!ELEMENT shortcut (explanation|details)*>
  
 <!ELEMENT check    (name,success,failure,explanation,details)>
 <!ELEMENT test     (name)>
  
 <!ELEMENT name          (#PCDATA|var|const)*>
 <!ELEMENT failure       (#PCDATA|var|const)*>
 <!ELEMENT success       (#PCDATA|var|const)*>
 <!ELEMENT explanation   (src*)>
 <!ELEMENT details       (para*)>
  
 <!ELEMENT src           (title,para+)>
 <!ELEMENT title         (#PCDATA|var|const)*>
 <!ELEMENT para          (#PCDATA|var|const|uri)*>
  
 <!ELEMENT var           EMPTY>
 <!ELEMENT const         EMPTY>
 <!ELEMENT uri           (#PCDATA)>
 <!ELEMENT tag           (#PCDATA)>
 <!ELEMENT section       (tag*)>
  
 <!ATTLIST msgcat       lang    CDATA #REQUIRED> 
  
 <!ATTLIST check        name    CDATA #REQUIRED>
 <!ATTLIST test         name    CDATA #REQUIRED>
  
 <!ATTLIST explanation  name    CDATA #IMPLIED
                        sameas  CDATA #IMPLIED>
 <!ATTLIST details      name    CDATA #IMPLIED
                        sameas  CDATA #IMPLIED> 
  
 <!ATTLIST src          type    CDATA #REQUIRED
                        from    CDATA #IMPLIED
                        fid     CDATA #IMPLIED>
 <!ATTLIST uri          link    CDATA #REQUIRED>
 <!ATTLIST var          name    CDATA #REQUIRED>
 <!ATTLIST var          display CDATA #IMPLIED>
 <!ATTLIST const        name    CDATA #REQUIRED>
 <!ATTLIST const        display CDATA #IMPLIED>
  
 <!ATTLIST section      name    CDATA #REQUIRED>
 <!ATTLIST tag          name    CDATA #REQUIRED>

Extending

Localization

All the localization files are located in the locale directory and the files have for suffix the corresponding langue. For example the main locale file for dnsdoctor will be dnsdoctor.en for a locale set to en_US.UTF-8.

The localization catalogue has been split into several files to make the addition of new input interfaces or tests easier, so the division is as follow:

  • DNSdoctor: dnsdoctor.*
  • Input interface: cli.* cgi.* inetd.* gtk.*
  • Tests: test/soa.* test/ns.* test/interop.* ...

Implementing new tests

Tests are stored in the directory test and are automatically loaded by dnsdoctor.

There are four types of tests, and the type of the test is defined by the module to which it belongs:

CheckGeneric 
The test doesn't need to know the nameserver nor the IP addresses.
No parameters are used by the method: chk_testname()
CheckNameServer 
The test need to know the nameserver.
One parameter is used by the method: chk_testname(ns)
CheckNetworkAddress 
The test need to know the nameserver and the IP address.
Two parameters are used by the method: chk_testname(ns, ip)
CheckExtra 
The test is not really DNS related and so doesn't need to know the nameserver nor the IP addresses.
No parameters are used by the method: chk_testname()


The method defining a test must also be part of a class, this allows creating intermediate functions that can be used by a set of related tests, and the class should inherit from Test.

There are two types of methods used by the test engine:

chk_name 
It is the check, if the return value is true the check was succesful, otherwise if the return value is false or an hash or an exception it is a failure. In the case of a hash, the key/value are used to perform a substitution in the message generated for tokent marked as variables; for the exception the message stored in it is displayed without localization as it is generally an error tied to the operating system.
tst_name 
It is a conditional, the method should return a string that will be used in a case statement.


The corresponding message catalog should be loaded for the set of tests that we are defining, this is done by adding the with_msgcat 'test/catname.%s' in the class, generally catname is the same name as the class in lowercase.


Below is an example implementing the check soa_ns_cname which check that the master used in the SOA records doesn't resolve into a CNAME, if it is the case the check fails and returns the name of the master and the name of the CNAME.

 require 'framework'
  
 module CheckNetworkAddress
     class SOA < Test
         with_msgcat 'test/soa.%s'
         def chk_soa_ns_cname(ns, ip)
             return true unless name = is_cname?(soa(ip).mname, ip)
             { 'master' => soa(ip).mname, 'alias' => name }
         end
     end
 end

And this is the fragment corresponding to the localization in english:

  <check name="soa_ns_cname">
    <name>SOA master is not an alias</name>
    <success>The SOA master is not an alias</success>
    <failure>SOA master is not allowed to point to a CNAME alias</failure>
    <explanation sameas="shortcut:ns_cname"/>
    <details><para>The master (<var name="master"/>) is a CNAME alias
      to <var name="alias"/>.</para></details>
  </check>

Adding input interface

New input interfaces should be part of the module Input and the class implementing it should be in uppercase, the file storing it in lowercase and in the directory zc/input, but the name should be the same (except for the case).

allow_preset 
Returns true if the interface is able to use preset values as stored in the dnsdoctor.conf configuration file.
initialize 
Initialise the class.
restart 
Method to call for restarting the input interface (must reinitialize internal values).
parse(param) 
Fill the parameter holder (param) from the parsing of primary input information.
interact(param, config, testmanager, io) 
Allow a deeper interaction with the user, the information available are the parameters (param), the configuration (config) provided by dnsdoctor.conf and the test manager (testmanager). And it is still possible to change the parameters
usage(errcode, io) 
Display usage normally using the io provided and exit if errcode is not nil.
error(str, errcode, io) 
Display the error provided as a string using the io provided and exit if errcode is not nil.


This is the structure of the class used by the CGI Input (for more information you can look at the various implementation in zc/input/):

 module Input
     class CGI
         with_msgcat "cgi.%s"
         def allow_preset                                  ; false ; end
         def initialize                                    ; ..... ; end
         def restart                                       ; ..... ; end
         def parse(p)                                      ; ..... ; end
         def interact(p, c, tm, io=$console.stdout)        ; ..... ; end
         def usage(errcode, io=$console.stdout)            ; ..... ; end
         def error(str, errcode=nil, io=$console.stdout)   ; ..... ; end
     end
 end

Adding publisher

New publisher should be part of the module Publisher and the class implementing it should inherit from Template, the file storing it should be in the zc/publisher directory. Unlike new tests or input interface, new publisher are not automatically loaded and should be added in the param.rb file.

error(text)  
Print the text in case of error.
intro(domain) 
Print a summary information about the domain being tested, generally the domain name and its nameservers.
diag_start() 
This method is called when dnsdoctor will start to send diagnostic messages (ie: calling diagnostic)
diagnostic1(domainname, i_count, i_unexp, w_count, w_unexp, f_count, f_unexp, res, severity) 
Print a one line (generally two lines) of information about the status of the domain domainname tested informations, warnings and fatals being counted and a flag is set if it was due to an exception (unexpected), res and severity holding information about the more significant message.
diagnostic(severity, testname, desc, lst) 
According to severity print a informational, warning or fatal message for the test testname, the description being given by desc and the list of host to which it appplies by lst.
status(domainname, i_count, w_count, f_count) 
Display the final status for the domain domainname informations, warnings and fatals being counted
setup(domain_name) 
Allow earlier initialisation. For HTML pages it is usefull for setting the page title in the head structure.


This is the structure of the class used by the Text publisher (for more information you can look at the various implementation in zc/publisher/):

 module Publisher
     class Text < Template
         Mime = "text/plain"
             
         def error(text)                                            ; ... ; end
         def intro(domain)                                          ; ... ; end
         def diag_start()                                           ; ... ; end
         def diag_section(title)                                    ; ... ; end
         def diagnostic1(domainname, i_count, i_unexp, 
                         w_count, w_unexp, f_count, f_unexp, res, severity) ; .. ;end
         def diagnostic(severity, testname, desc, lst)              ; ... ; end
         def status(domainname, i_count, w_count, f_count)          ; ... ; end
         def setup(domain_name)                                     ; ... ; end
     end
 end