DNS programming

Brian {Hamilton Kelly} bhk at dsl.co.uk
Sun Sep 5 14:24:36 UTC 1999


On 1 Sep, in article <19990901.1629.27990snz at dsl.co.uk> I wrote:

> Just for amusement, I wrote something in REXX that would query a
> nameserver (a sort-of cut-down "nslookup"); I could e-mail you that
> source if you're interested.  (I did this because I was writing an SMTP
> client in Rexx, and wanted to access the destinations' MX records from
> within the code, using neither a smarthost, nor calling the local
> resolver.)  It was quite an "interesting" exercise getting the correct
> binary values into character strings (because the latter is the ONLY form
> of variable that Rexx uses).  Nevertheless, it worked: I really must get
> around to completing the SMTP client (I haven't touched it for a year:-)

At the suggestiong of Joseph Yao, I'm posting the code here; please
remember that this is part of a work-in-progress, and not really ready to
see the light of day.  There are many areas in which it could be
improved, but I was only writing it in the first place as a "feasibility
study".  I've (today) edited in a few comments that indicate some of the
improvements that it urgently requires.  

[Apologies to the person to whom I'd already mailed (at his request) a
copy: I should have checked it first to see how it operated, because it
turns out that one had an IP address built in to it for a nameserver that
is inaccessible from the 'Net at large, being behind a firewall.]

Anyway, here it is, for what it's worth.  If anyone cares to come up with
some "really nifty" improvements, please e-mail them to me.  I've cross-
posted this article to comp.land.rexx as well, so that the experienced
Rexx programmers there can suggest improvements.

One final caution: if you call the file nslookup.cmd, it's going to hide
the genuine nslookup.exe program from you --- I've been calling it
nsl.cmd instead.

/* NSLookup.cmd                                                              */
/* ~~~~~~~~~~~~                                                              */
/* A REXX command procedure that emulates some features of the program       */
/* nslookup, as an experiment to test the feasibility of querying a name     */
/* server from REXX.                                                         */
/*                                                                           */
/* This command procedure is a "work-in-progress"; eventually, it will       */
/* form part of a suite of programs for handling various TCP/IP protocols    */
/* which will be released together under the terms of the Gnu Public         */
/* Licence.  Therefore treat this fragment as being bound by those terms.    */
/*                                                                           */
/* Author:        B Hamilton Kelly                                           */
/* E-mail:        bhk at dsl.co.uk                                              */
   Version     =  0.23
/* Last-Modified: 05-Sep-1999                                                */

/*********************************************************/
/*   Modification History                                */
/*********************************************************/
/* 0.10  980514  Initial scratchings in the dark!        */
/* 0.20  980525  Got responses onto one line             */
/* 0.21  980527  Get SOA, MX and ANY in 3 requests       */
/* 0.22  980708  Write "> " prompt without trailing CRLF */
/* 0.23  990905  Minor tidying up for publication        */
/*********************************************************/

/* An obvious improvement to the program would be for it to read the
   resolv (or resolv2) file and determine the recommended nameservers that
   way; for the time being, the IP address is built-in, through the
   following assignment: if your nameserver is anywhere other than on the
   local host, you'll have to modify this: */

nameserver = '127.0.0.1'  /* IP address of NS as dotted-quad string */


parse arg DomainName

/* The program assumes that there is only one parameter; giving a second
to specify an alternate name server would be a handy improvement */

if DomainName == "" then
    Interactive = 1
else
  Interactive = 0

QType = c2x('00FF'X)   /* QueryType  = "ANY" */
QClass = c2x('00FF'x)  /* QueryClass = "ANY" */
ZERO  = c2x('0000'X)   /* Number 0 in 16-bit word */
ONE   = c2x('0001'X)   /* Number 1 ditto */
TWO   = c2x('0002'X)   /* 2 ditto */
THREE = c2x('0003'X)   /* 3 ditto */

NUMERIC DIGITS 11

NextSerial = 0

Message = ""    /* GLOBAL, since it might contain, e.g. ',' characters       */

Assembly = ""   /* GLOBAL, since it's used recursively */

Name = ""       /* GLOBAL, since passes back result */

Query = b2x('0000000100000000') /*  QR = 0    --- this is a query
                                    OP = 0000 --- a standard QUERY
                                    AA = 0    --- (authoritative answer)
                                    TC = 0    --- (truncated)
                                    RD = 1    --- Recursion Desired
                                    RA = 0    --- (recursion available)
                                    Z  = 000  --- (reserved field)
                                    RC = 0000 --- (response code)            */

Call RxFuncAdd 'SockLoadFuncs', 'RxSock', 'SockLoadFuncs'
Call SockLoadFuncs('QUIET')

/* Get a socket for initiating connections */
socket=SockSocket('AF_INET', 'SOCK_DGRAM', 0 )
If socket < 0 Then Do
  Say 'SockSocket()' errno
  Exit errno
End
Say 'Created socket' socket

NShost. = ""
NShost.!family = "AF_INET"
NShost.!port = 53                       /* Well-known port for DNS */
NShost.!addr = nameserver

stat = SockConnect( socket, "NShost.!" )
If stat < 0 Then Do
  Say 'SockConnect()' errno
  Signal Halt
End
Say 'Connected' socket 'to' NShost.!addr || ':' || NShost.!port

Signal On Halt                          /* Trap Ctrl-Break */

Do forever
  if Interactive > 0 then do
    Call CHAROUT ,"> "
    parse pull DomainName
    parse upper var DomainName testVal
    if testVal == "EXIT" then
      Leave
  end /* do */


  NextSerial = NextSerial + 1

  Header = x2c( d2x(NextSerial,4) ) || x2c(Query) || x2c( ONE ) /* QDCOUNT=1 */
  Header = Header || x2c( ZERO ) || x2c( ZERO ) || x2c( ZERO )

  LabelPart = MakeLabel( DomainName )
  Say "LabelPart length =" length(LabelPart)

  Question = LabelPart || x2c(c2x('0006'X)) || x2c(ONE) /* type=SOA class=IN */
  Say "Question length =" length(Question)
  Request = Header || Question
  Say "Request length =" length(Request)

  Call PrintMessage Request

  stat = SockSend( socket, Request )
  if stat < 0 then do
     Call SockPsock_errno("SockSend")
     Signal Halt
  end /* do */

  Say "Request sent OK"

  reads.0 = 1; reads.1 = socket

  stat = SockSelect( "reads.", "", "", 10 )      /* Wait up to 10 secs for response */
  if stat = 0 then do                           /* Request timed out */
    Say "Request timed out :-("
    Signal Halt
  end /* do */

  Response = ""
  received = SockRecv( socket, "Response", 1024 )
  if received < 0 then do
    SockPsock_errno("SockRecv")
    Signal Halt
  end /* do */
  if length(Response) = 0 then do               /* Nothing useful */
    Say "Nothing in response buffer :-("
    Signal Halt
  end /* do */

  Call PrintMessage Response


  NextSerial = NextSerial + 1

  Header = x2c( d2x(NextSerial,4) ) || x2c(Query) || x2c( ONE ) /* QDCOUNT=1 */
  Header = Header || x2c( ZERO ) || x2c( ZERO ) || x2c( ZERO )

  Question = LabelPart || x2c(c2x('000F'X)) || x2c(ONE)   /* type=MX class=IN     */
  Say "Question length =" length(Question)
  Request = Header || Question
  Say "Request length =" length(Request)

  Call PrintMessage Request

  stat = SockSend( socket, Request )
  if stat < 0 then do
     Call SockPsock_errno("SockSend")
     Signal Halt
  end /* do */

  Say "Request sent OK"

  reads.0 = 1; reads.1 = socket

  stat = SockSelect( "reads.", "", "", 10 )      /* Wait up to 10 secs for response */
  if stat = 0 then do                           /* Request timed out */
    Say "Request timed out :-("
    Signal Halt
  end /* do */

  Response = ""
  received = SockRecv( socket, "Response", 1024 )
  if received < 0 then do
    SockPsock_errno("SockRecv")
    Signal Halt
  end /* do */
  if length(Response) = 0 then do               /* Nothing useful */
    Say "Nothing in response buffer :-("
    Signal Halt
  end /* do */

  Call PrintMessage Response

  NextSerial = NextSerial + 1

  Header = x2c( d2x(NextSerial,4) ) || x2c(Query) || x2c( ONE ) /* QDCOUNT=1 */
  Header = Header || x2c( ZERO ) || x2c( ZERO ) || x2c( ZERO )

  Question = LabelPart || x2c(QType) || x2c(QClass)    /* type=ANY class=ANY   */
  Say "Question length =" length(Question)
  Request = Header || Question
  Say "Request length =" length(Request)

  Call PrintMessage Request

  stat = SockSend( socket, Request )
  if stat < 0 then do
     Call SockPsock_errno("SockSend")
     Signal Halt
  end /* do */

  Say "Request sent OK"

  reads.0 = 1; reads.1 = socket

  stat = SockSelect( "reads.", "", "", 10 )      /* Wait up to 10 secs for response */
  if stat = 0 then do                           /* Request timed out */
    Say "Request timed out :-("
    Signal Halt
  end /* do */

  Response = ""
  received = SockRecv( socket, "Response", 1024 )
  if received < 0 then do
    SockPsock_errno("SockRecv")
    Signal Halt
  end /* do */
  if length(Response) = 0 then do               /* Nothing useful */
    Say "Nothing in response buffer :-("
    Signal Halt
  end /* do */

  Call PrintMessage Response

  Say ''

  if Interactive = 0 then
    Leave

end /* do */

/* To trap Ctrl-Break, or after other error with an open socket */

Halt:
  Call SockShutDown socket, 2
  Call SockClose socket
  Exit

/* This function returns the "label" part for a DNS query; each element of   */
/* the fully-qualified domain name is separated out, and preceded by a single*/
/* byte showing the length of that element.                                  */
/* The complete label is terminated with a length byte of zero, representing */
/* the null element before the final period designating the root of the DNS. */

MakeLabel:  Procedure

  parse arg Adomain

  Label = ""

  if right(Adomain,1) \= '.' then               /* Append final period */
    Adomain = Adomain || '.'

  parse var Adomain Element '.' Adomain
  do while length(Element) > 0
    Label = Label || D2C(length(Element)) || Element
    parse var Adomain Element '.' Adomain
  end /* do */
  Label = Label || D2C(0)

  return Label

/* For testing purposes, this prints out a "label"                           */

PrintLabel: Procedure Expose Message Assembly Name

  parse arg ResultName, CurPos, Clear

  if CurPos == "" then
    CurPos = 1

  if Clear = "" then 
    Assembly = ""

  do forever
    LenByte = C2D(substr(Message,CurPos,1)); CurPos = CurPos + 1
    if LenByte > 63 then       /* A "pointer" to a label fragment */
    do 
      if LenByte < 192 then Do
         Say "Illegal <label-length> marker (" || LenByte || ") at" CurPos-1
         Exit
      end /* do */
      LenByte = (LenByte - 192)*256 + c2d(substr(Message,CurPos,1))
      CurPos = CurPos+1; LenByte = LenByte + 1
      INTERPRET 'Call PrintLabel "'ResultName'",' LenByte', "Keep"'
      Return CurPos  /* NB We do NOT "follow" the pointer */
    end /* do */

    if LenByte = 0 then do
       Assembly = Assembly || '.'
       INTERPRET ResultName '= Assembly'
       Leave
    end /* do */

    Element = substr(Message,CurPos,LenByte); CurPos = CurPos + LenByte
    if Assembly = "" then
      Assembly = Element
    else
      Assembly = Assembly || '.' || Element

  end /* do */

  Return CurPos

/* Function GetString( posn ) return new_posn = string                      */
GetString: Procedure Expose Message

  Parse Arg CurPos

  LenByte = C2D(substr(Message,CurPos,1)); CurPos = CurPos + 1
  Data = substr(Message,CurPos,LenByte)
  NewPos = CurPos + LenByte
  Return NewPos || ' = "' || Data || '"'


/* Subroutine PrintMessage (Message)                                         */
/* Parses and prints out entire message structure                            */

/* There are great /swathes/ of debugging information printed here; a
   useful modification would be to make these optional. */
   
PrintMessage: Procedure

  Parse Arg Message

  QDcount = 0; ANcount = 0; NScount = 0; ARcount = 0

/* This is where the raw bytes of a message are printed; this debugging
   information can be omitted by altering the comment indicators that
   preced and follow the block of code, such that it is commented out.
   All the same, it would be better under some form of user control. */
   
/* */
  Size = length(Message); Offset = 1
  do while Size > 0
    ShortWord = c2x(substr(Message,Offset,2))
    Say Offset || ": '" || ShortWord || "'X"
    Offset = Offset + 2; Size = Size - 2
  end /* do */
/* */

  NextPart = PrintHeader( Message )

  if QDcount > 0 then
    Say "Questions:"
  NextPart = PrintQuestions(  NextPart, QDcount )

  if ANcount > 0 then
    Say "Answers:"
  NextPart = PrintAnswers(  NextPart, ANcount )

  if NScount > 0 then
    Say "Authoritative information may be obtained from:"
  NextPart = PrintAnswers(  NextPart, NScount )

  if ARcount > 0 then
    Say "Additional Information:"
  NextPart = PrintAnswers(  NextPart, ARcount )

  Return

/* Function PrintHeader (Message) returns pointer to next segment            */
PrintHeader: Procedure Expose QDcount ANcount NScount ARcount

  Parse Arg Message

  CurPos = 1
  Say "CurPos =" CurPos || "; Message length =" length(Message)

  SeqNo = c2d(substr(Message,CurPos,2)); CurPos = CurPos + 2
  Flags = x2b(c2x(substr(Message,CurPos,2))); CurPos = CurPos + 2
  QDcount = c2d(substr(Message,CurPos,2)); CurPos = CurPos + 2
  ANcount = c2d(substr(Message,CurPos,2)); CurPos = CurPos + 2
  NScount = c2d(substr(Message,CurPos,2)); CurPos = CurPos + 2
  ARcount = c2d(substr(Message,CurPos,2)); CurPos = CurPos + 2

  QR = substr(Flags,1,1)
  Opcode = x2d(b2x(substr(Flags,2,4)))
  AA = substr(Flags,6,1)
  TC = substr(Flags,7,1)
  RD = substr(Flags,8,1)
  RA = substr(Flags,9,1)
  Z = substr(Flags,10,3)
  Rcode = substr(Flags,13,4)

  Say "Sequence number:" SeqNo
  if QR = '1' then
    Say "   is a response."
  else
    Say "   is a query."
  select 
     when Opcode = 0 then
       Say "Standard QUERY"
     when Opcode = 1 then
       Say "Inverse IQUERY"
     when Opcode = 2 then
       Say "Server STATUS request"
  otherwise
    Say "Unexpected OPcode" Opcode "in query"
  end  /* select */

  if AA = '0' then
    Auth = "Non-authoritative"
  else
    Auth = "Authoritative"
  Say Auth "response"

  if TC = '1' then
    Say "Response was truncated :-("
  
  if RD = '1' then
    Say "Recursion requested"

  if RA = '1' then
    Say "Recursion available"

  if Z \= 0 then
    Say "Reserved field Z is non-zero!  (" || Z || ")"
  
  select
     when Rcode = 0 then
       Say "No error condition"
     when Rcode = 1 then
       Say "Format ERROR"
     when Rcode = 2 then
       Say "Server failure"
     when Rcode = 3 then
       Say "Name ERROR; no such domain"
     when Rcode = 4 then
       Say "Server does not support requested query"
     when Rcode = 5 then
       Say "Request refused by server"
  otherwise
    Say "Unexpected Response Code (" || Rcode || ")"
  end  /* select */

  Say "There are" QDcount "questions"

  Say "There are" ANcount "answers"

  Say "There are" NScount "NS resource records in authority records section"

  Say "There are" ARcount "additional records"

  Return CurPos

/* Function PrintQuestions ( Pointer, Number) return next pointer    */

PrintQuestions: Procedure Expose Message Name

  Parse Arg  CurPos, Count

  do Qct = 1 to Count
    Say "Question" Qct
    CurPos = PrintQuestion(  CurPos )
  end /* do */

  Return CurPos


/* Function PrintQuestion ( Pointer) return pointer to next question */

PrintQuestion: Procedure Expose Message Name

  Parse Arg  CurPos

  CurPos = PrintLabel("Name",CurPos)
  Say Name

  Qtype =c2d(substr(Message,CurPos,2)); Qclass =c2d(substr(Message,CurPos+2,2))

  select
     when Qtype = 252 then
       Say "qtype = AXFR"
     when Qtype = 253 then
       Say "qtype = MAILB"
     when Qtype = 254 then
       Say "qtype = MAILA"
     when Qtype = 255 then
       Say "qtype = ANY"
  otherwise
    Call PrintType Qtype
  end  /* select */

  select
     when Qclass = 255 then
       Say "query class = ANY"
  otherwise
    Call PrintClass Qclass
  end  /* select */

  Return CurPos + 4

/* Subroutine PrintType (type)                                               */

PrintType: Procedure

  Parse Arg TypeFlag

  select
     when TypeFlag = 1 then
       Say "type = A"
     when TypeFlag = 2 then
       Say "type = NS"
     when TypeFlag = 3 then
       Say "type = MD"
     when TypeFlag = 4 then
       Say "type = MF"
     when TypeFlag = 5 then
       Say "type = CNAME"
     when TypeFlag = 6 then
       Say "type = SOA"
     when TypeFlag = 7 then
       Say "type = MB"
     when TypeFlag = 8 then
       Say "type = MG"
     when TypeFlag = 9 then
       Say "type = MR"
     when TypeFlag = 10 then
       Say "type = NULL"
     when TypeFlag = 11 then
       Say "type = WKS"
     when TypeFlag = 12 then
       Say "type = PTR"
     when TypeFlag = 13 then
       Say "type = HINFO"
     when TypeFlag = 14 then
       Say "type = MINFO"
     when TypeFlag = 15 then
       Say "type = MX"
     when TypeFlag = 16 then
       Say "type = TXT"
  otherwise
    Say "ERROR: unknown type =" TypeFlag
  end  /* select */

  Return

/* Subroutine PrintClass (class)                                             */

PrintClass: Procedure

  Parse Arg Class

  select
     when Class = 1 then
       Say "class = IN"
     when Class = 2 then
       Say "class = CSNET"
     when Class = 3 then
       Say "class = CHAOS"
     when Class = 4 then
       Say "class = HESIOD"
  otherwise
    Say "ERROR: unknown Class =" Class
  end  /* select */

  Return

/* Function PrintAnswers ( Pointer, Number) return next pointer    */

PrintAnswers: Procedure Expose Message Name

  Parse Arg CurPos, Count

  do Act = 1 to Count
    CurPos = PrintRR(  CurPos )
  end /* do */

  Return CurPos

/* Function PrintRR( Pointer) return pointer to next Resource Record */

PrintRR: Procedure Expose Message Name

  Parse Arg CurPos

  CurPos = PrintLabel("Name",CurPos)

  RRtype = c2d(substr(Message,CurPos,2))
  RRclass = c2d(substr(Message,CurPos+2,2))
  TTL = c2d(substr(Message,CurPos+4,4))
  RdLength = c2d(substr(Message,CurPos+8,2))
  CurPos = CurPos + 10

  Call PrintRdata Name, TTL, RRtype, RRclass, CurPos, RdLength

  Return CurPos + RdLength

/* Subroutine PrintRdata( label, TTL, type, class, posn, count )                         */

PrintRdata: Procedure Expose Message Name

  Parse Arg ThisLabel, TTL, Rtype, Rclass, Posn, Count

  select
     when Rclass = 1 then
       Class = "IN"
     when Rclass = 2 then
       Class = "CSNET"
     when Rclass = 3 then
       Class = "CHAOS"
     when Rclass = 4 then
       Class = "HESIOD"
  otherwise
    Say "ERROR: unknown Class =" Rclass
    Return
  end  /* select */

  select
     when Rtype = 5 then do             /* CNAME */
       Call PrintLabel "Name", Posn
       Say ThisLabel  TTL  Class "CNAME" Name
     end /* do */
     when Rtype = 13 then do            /* HINFO */
       Parse Value GetString( Posn ) with Posn "=" CPU
       Parse Value GetString( Posn ) with Posn "=" OS
       Say ThisLabel TTL Class "HINFO" "CPU:" CPU "OS:" OS
     end /* do */
     when Rtype = 7 then do             /* MB (experimental) */
       Call PrintLabel "Name", Posn
       Say ThisLabel TTL Class "MB" Name
     end /* do */
     when Rtype = 3 then do             /* MD (Obsolete) */
       Call PrintLabel "Name", Posn
       Say ThisLabel TTL Class "MD" Name
     end /* do */
     when Rtype= 4 then do              /* MF (Obsolete) */
       Call PrintLabel "Name", Posn
       Say ThisLabel TTL Class "MF" Name
     end /* do */
     when Rtype = 8 then do             /* MG (Experimental) */
       Call PrintLabel "Name", Posn
       Say ThisLabel TTL Class "MG" Name
     end /* do */
     when Rtype = 14 then do            /* MINFO (Experimental) */
       Posn = PrintLabel( "Name", Posn )
       mailbox = Name
       Call PrintLabel "Name", Posn
       mailhost = Name
       Say ThisLabel TTL Class "MINFO" mailbox || "@" || mailhost
     end /* do */
     when Rtype = 9 then do             /* MR (Experimental) */
       Call PrintLabel "Name", Posn
       Say ThisLabel TTL Class "MR" Name
     end /* do */
     when Rtype = 15 then do            /* MX */
       Preference = c2d(substr(Message,Posn,2))
       Call PrintLabel "Name", Posn+2
       Say ThisLabel TTL Class "MX" Preference Name
     end /* do */
     when Rtype = 10 then               /* NULL */
       Say ThisLabel TTL Class "NULL"
     when Rtype = 2 then do             /* NS */
       Call PrintLabel "Name", Posn
       Say ThisLabel TTL Class "NS" Name
     end /* do */
     when Rtype = 12 then do            /* PTR */
       Call PrintLabel "Name", Posn
       Say ThisLabel TTL Class "PTR" Name
     end /* do */
     when Rtype = 6 then do             /* SOA */
       Posn = PrintLabel( "Name", Posn )                /* Primary server */
       Say ThisLabel TTL Class "SOA"  Name
       Posn = PrintLabel( "Name", Posn )                /* Responsible person */
       Say "                                 " Name '('
       Serial = c2d( substr(Message,Posn,4), 5 ); Posn = Posn + 4
       Say "                                 " Serial || ';  Serial'
       Refresh = c2d(substr(Message,Posn,4)); Posn = Posn + 4
       Say "                                 " Refresh || '; Refresh'
       Retry = c2d(substr(Message,Posn,4)); Posn = Posn + 4
       Say "                                 " Retry || ';   Retry'
       Expire = c2d(substr(Message,Posn,4)); Posn = Posn + 4
       Say "                                 " Expire || ';  Expire'
       Minimum = c2d(substr(Message,Posn,4)); Posn = Posn + 4
       Say "                                 " Minimum || ';   Minimum)'
     end /* do */
     when Rtype = 16 then do            /* TXT */
       Parse Value GetString(Posn) with Posn '=' Text
       Say ThisLabel TTL Class "TXT" Text
     end /* do */
     when Rtype = 1 then do             /* A */
       a = c2d(substr(Message,Posn,1)); b = c2d(substr(Message,Posn+1,1))
       c = c2d(substr(Message,Posn+2,1)); d = c2d(substr(Message,Posn+3,1))
       Say ThisLabel TTL Class "A" a || '.' || b || '.' || c || '.' || d
     end /* do */
     when Rtype = 11 then do            /* WKS */
       a = c2d(substr(Message,Posn,1)); b = c2d(substr(Message,Posn+1,1))
       c = c2d(substr(Message,Posn+2,1)); d = c2d(substr(Message,Posn+3,1))
       protocol = c2d(substr(Message,Posn+4,2))
       Say ThisLabel TTL Class "WKS" a || '.' || b || '.' || c || '.',
                                       || d || ':' || protocol
       bitmap = x2b(c2x(substr(Message,Posn+6,Count-6)))
       Say bitmap
     end /* do */
  otherwise
    Say "Unknown RR Type =" Rtype
  end  /* select */
  
  Return 



-- 
Brian {Hamilton Kelly}                                         bhk at dsl.co.uk
    "But we're a university.  We /have/ to have a library!..."said Ridcully,
         "What sort of people would we be if we didn't go into the library?"
    "Students", said the Senior Wrangler, morosely. [TP: The Last Continent]



More information about the bind-users mailing list