DNS over UDP extension

This is a DNS client over the UDP extension - 1Nov2011 CMcC

It provides a full and a simple DNS client class.

# dns query using Udp extension
# Colin McCormack

lappend auto_path [pwd]/Udp
package require Udp
package require TclOO
package provide Dns 1.1

oo::class create DNS {
    method decodeName {varname} {
        upvar 1 $varname text
        set line {}
        set suffix ""
        while {$text ne ""} {
            binary scan $text ca* len text
            if {$len < 0} {
                # compression
                binary scan $text ca* offset text
                set offset [expr {($len & 0x3F)<<8 | ($offset & 0xFF)}]
                variable response
                set reference [string range $response $offset end]
                set suffix .[my decodeName reference]
                break
            } elseif {$len} {
                lappend line [string range $text 0 $len-1]
                set text [string range $text $len end]
            } else {
                break
            }
        }
        return [join $line .]$suffix
    }

    method decodeQD {varname qdcount} {
        upvar 1 $varname text
        set result {}
        while {$qdcount} {
            incr qdcount -1
            set name [my decodeName text]
            binary scan $text SSa* qtype qclass text
            lappend result [list $name $qtype $qclass]
        }
        return $result
    }

    method decodeAddress {address} {
        catch {
            binary scan $address cccc a1 a2 a3 a4
            foreach v {a1 a2 a3 a4} {
                set $v [expr { [set $v] & 0xff }]
            }
            set result [join [list $a1 $a2 $a3 $a4] .]
        } e eo
        return $result
    }

    method rdA {rd} {
        # a host address
        return [my decodeAddress $rd]
    }

    method rdNS {rd} {
        # an authoritative name server
        return [my decodeName rd]
    }

    method rdMD {rd} {
        # a mail destination (Obsolete - use MX)
        return [my decodeName rd]
    }

    method rdMF {rd} {
        # a mail forwarder (Obsolete - use MX)
        return [my decodeName rd]
    }

    method rdCNAME {rd} {
        # the canonical name for an alias
        return [my decodeName rd]
    }

    method rdSOA {rd} {
        # marks the start of a zone of authority
        set mname [my decodeName rd]
        set rname [my decodeName rd]
        binary scan $rd IIIII serial refresh retry expire minimum
        return [list mname $mname rname $rname serial $serial refresh $refresh retry $retry expire $expire minimum $minimum]
    }

    method rdMB {rd} {
        # a mailbox domain name (EXPERIMENTAL)
        return [my decodeName rd]
    }

    method rdMG {rd} {
        # a mail group member (EXPERIMENTAL)
        return [my decodeName rd]
    }

    method rdMR {rd} {
        # a mail rename domain name (EXPERIMENTAL)
        return [my decodeName rd]
    }

    method rdNULL {rd} {
        # a null RR (EXPERIMENTAL)
        return $rd
    }

    method rdWKS {rd} {
        # a well known service description
        binary scan $rd Ica* address protocol rd
        set address [my decodeAddress $rd]
        return [list address $address protocol $protocol bitmap $rd]
    }

    method rdPTR {rd} {
        # a domain name pointer
        return [my decodeName rd]
    }

    method rdHINFO {rd} {
        # host information
        set cpu [my decodeName rd]
        set os [my decodeName rd]
        return [list cpu $cpu os $os]
    }

    method rdMINFO {rd} {
        # mailbox or mail list information
        set rmailbx [my decodeName rd]
        set emailbx [my decodeName rd]
        return [list rmailbx $rmailbx emailbx $emailbx]
    }

    method rdMX {rd} {
        # mail exchange
        binary scan $rd S preference rd
        return [list preference $preference [my decodeName rd]]
    }

    method rdTXT {rd} {
        # text strings
        set result {}
        while {$rd ne ""} {
            lappend result [my decodeName rd]
        }
        return $result
    }

    method rr {varname count} {
        upvar 1 $varname text
        set result {}

        while {$count && $text ne ""} {
            incr count -1
            set name [my decodeName text]
            binary scan $text SSISa* type class ttl rdlength text
            set type [string trimleft $type 0]
            variable qtypes; catch {set type [dict get $qtypes $type]}
            set class [string trimleft $class 0]
            variable qclasses; catch {set class [dict get $qclasses $class]}

            set rdata [string range $text 0 $rdlength-1]
            catch {set rdata [my rd$type $rdata]}

            set text [string range $text $rdlength end]

            lappend result [list name $name type $type class $class ttl $ttl rdata $rdata]
        }

        return $result
    }

    method decode {payload from port} {
        variable response $payload

        binary scan $payload SB8B8SSSS id h1 h2 qdcount ancount nscount arcount
        lassign [split $h1 ""] qr . . . . aa tc rd
        set rcode 0b0[string trimleft [string range $h2 4 end] 0]
        set rcode [expr {$rcode + 0}]
        set ra [string index $h2 0]
        set id [string trimleft $id 0]
        set result [list id $id qr $qr aa $aa tc $tc rd $rd ra $ra rcode $rcode qdcount $qdcount ancount $ancount nscount $nscount arcount $arcount]
        if {$rcode} {
            variable rcodes
            dict set result error [dict get $rcodes $rcode]
        }

        set content [string range $payload 12 end]

        dict set result qd [my decodeQD content $qdcount]
        dict set result an [my rr content $ancount]
        dict set result ns [my rr content $nscount]
        dict set result ar [my rr content $arcount]
        if {$content ne ""} {
            dict set result remainder $content
        }
        #puts stderr "RESPONSE: $result"
        return $result
    }

    # response - get result and invoke callback
    method response {payload from port chan} {
        if {[catch {
            my decode $payload $from $port
        } result eo]} {
            puts stderr "$result ($eo)"
        } else {
            # got a complete response - invoke callback for it.
            variable callbacks
            set id [dict get $result id]
            if {[dict exists $callbacks $id]} {
                set callback [dict get $callbacks $id]
                dict unset callbacks $id
                {*}$callback $result
            }
        }
    }

    method qhead {id {opcode QUERY} {recurse 1}} {
        if {$recurse} {
            set h 0x0100
        } else {
            set h 0
        }
        switch $opcode {
            QUERY {}
            IQUERY {
                set h [expr {$h | 0x10}]
            }
            STATUS {
                set h [expr {$h | 0x100}]
            }
            default {
                error "opcode must be one of QUERY, IQUERY, STATUS"
            }
        }
        return [binary format SS $id $h]
    }

    method dquery {name {qtype A} {qclass IN}} {
        variable qtypes; variable qclasses
        set query ""
        foreach label [split $name .] {
            set len [string length $label]
            if {$len > 255} {
                error "name component too long: '$label'"
            }
            append query [binary format ca$len $len $label]
        }
        append query \0
        append query [binary format SS [dict get $qtypes $qtype] [dict get $qclasses $qclass]]
        return $query
    }

    method dqueries {id args} {
        set count 0
        set opcode QUERY
        set qtype A
        set qclass *
        set recurse 1
        set query ""
        set skip 0

        foreach arg $args {
            if {$skip} {
                set $var $arg
                set skip 0
                continue
            }
            switch $arg {
                -qtype -
                -qclass -
                -opcode -
                -callback -
                -recurse {
                    incr skip
                    set var [string trim $arg -]
                }
                default {
                    append query [my dquery $arg $qtype $qclass]
                    incr count
                }
            }
        }

        if {[info exists callback]} {
            variable callbacks; dict set callbacks $id $callback
        }

        set query "[my qhead $id $opcode $recurse][binary format SSSS $count 0 0 0]$query"
        return $query
    }

    method query {dns args} {
        variable udp; variable domain
        variable qcount; incr qcount
        set query [my dqueries $qcount {*}$args]
        udp::send $udp $dns $domain $query
    }

    destructor {
        variable udp
        catch {chan close $udp}
    }

    constructor {args} {
        variable domain 53
        variable {*}$args

        variable qcount 1
        variable udp [::udp create 0 [list [self] response]]

        variable callbacks {}
        variable rcodes {
            0 {No error condition}
            1 {Format error - The name server was unable to interpret the query.}
            2 {Server failure - The name server was unable to process this query due to a problem with the name server.}
            3 {Name Error - Meaningful only for responses from an authoritative name server, this code signifies that the domain name referenced in the query does not exist.}
            4 {Not Implemented - The name server does not support the requested kind of query.}
            5 {Refused - The name server refuses to perform the specified operation for policy reasons.  For example, a name server may not wish to provide the information to the particular requester, r a name server may not wish to perform a particular operation (e.g., zone transfer) for particular data.}
        }

        variable qtypes
        foreach {n v d} {
            A 1 {a host address}
            NS 2 {an authoritative name server}
            MD 3 {a mail destination (Obsolete - use MX)}
            MF 4 {a mail forwarder (Obsolete - use MX)}
            CNAME 5 {the canonical name for an alias}
            SOA 6 {marks the start of a zone of authority}
            MB 7 {a mailbox domain name (EXPERIMENTAL)}
            MG 8 {a mail group member (EXPERIMENTAL)}
            MR 9 {a mail rename domain name (EXPERIMENTAL)}
            NULL 10 {a null RR (EXPERIMENTAL)}
            WKS 11 {a well known service description}
            PTR 12 {a domain name pointer}
            HINFO 13 {host information}
            MINFO 14 {mailbox or mail list information}
            MX 15 {mail exchange}
            TXT 16 {text strings}
            AXFR 252 {A request for a transfer of an entire zone}
            MAILB 253 {A request for mailbox-related records (MB, MG or MR)}
            MAILA 254 {A request for mail agent RRs (Obsolete - see MX)}
            * 255 {A request for all records}
        } {
            dict set qtypes $n $v
            dict set qtypes $v $n
        }

        variable qclasses 
        foreach {n v d} {
            IN 1 {the Internet}
            CS 2 {the CSNET class (Obsolete - used only for examples in some obsolete RFCs)}
            CH 3 {the CHAOS class}
            HS 4 {Hesiod [Dyer 87]}
            *               255 {any class}
        } {
            dict set qclasses $n $v
            dict set qclasses $v $n
        }
    }
}

oo::class create DNSsimple {
    superclass DNS

    method response {payload from port} {
        if {[catch {
            my decode $payload $from $port
        } result eo]} {
            puts stderr "$result ($eo)"
        } else {
            # got a complete response - invoke callback for it.
            variable callbacks
            set id [dict get $result id]
            if {[dict exists $callbacks $id]} {
                if {[dict exists $result error]} {
                    # got an error
                    set lookup [list [dict get $result error]]
                } else {
                    # post-process the result to just give simplified answers
                    set lookup {}
                    foreach {v} [dict get $result an] {
                        dict lappend lookup [string trimleft [dict get $v name] .] [dict get $v rdata]
                    }
                }

                set callback [dict get $callbacks $id]
                dict unset callbacks $id
                {*}$callback {*}$lookup
            }
        }
    }

    method reverse {dns args} {
        set skip 0
        set rquery {-qtype PTR}
        foreach arg $args {
            if {$skip} {
                set skip 0
            } elseif {[string match -* $arg]} {
                if {$arg eq "-qtype"} {
                    error "reverse queries cannot specify -qtype"
                }
                set skip 1
            } else {
                # got to be an IP address
                set arg [join [lreverse [split $arg .]] .].in-addr.arpa
            }
            lappend rquery $arg
        }
        my query $dns {*}$rquery
    }

    constructor {args} {
        next {*}$args
    }
}

if {[info exists argv0] && ($argv0 eq [info script])} {
    set dns 192.168.178.2        ;# your dns server

    proc putss {where args} {
        puts $where DNS:$args
    }

    # simple DNS query processing
    DNSsimple create dnss
    dnss query $dns -callback {putss stderr} google.com        ;# make a simple query
    dnss reverse $dns -callback {putss stderr} 74.125.237.48
    dnss query $dns -callback {putss stderr} localhost
    dnss query $dns -callback {putss stderr} google.com        localhost

    # full DNS query processing
    DNS create dns
    dns query $dns -callback {puts stderr} thighbone        ;# make a query
    dns query $dns -qtype A -callback {puts stderr} google.com        ;# make a query

    vwait forever
}