DNS NXDOMAIN Handler - No More Non-Existent Domain

Problem this snippet solves:

With this rules, BIGIP can convert a “non-exist domain” DNS query result to a "normal" response to client. That will help if client type the wrong domain name (ie. if a client types "wwww.f5.com" for www.f5.com, when the request send to a load balanced DNS cache server, it will return an error message of non-exist domain (NXDOMAIN), then the plugin of browser will redirect it to an search engine or other site. After apply this rules to the BIGIP that load balances the DNS cache servers, BIGIP will replace the Cache DNS’s error message and turn it to a specified IP address like "wwww.f5.com=200.100.4.10" in the sample rule. That the 200.100.4.10 may be portal of the service provider. Or a friendly error page that indicate the type error.

Note: Rewriting non-existent domain DNS responses can introduce serious security issues for any domain which is resolved in such a manner. It may also break SPAM blacklisting. For more information, see http://www.wired.com/threatlevel/2008/04/isps-error-page

The following iRule requires BIG-IP version 11 with a DNS profile attached to the virtual server.

when DNS_RESPONSE {
 if { [DNS::ptype] == "NXDOMAIN" } {
    DNS::answer insert "[DNS::question name]. 60 [DNS::question class] [DNS::question type] 200.100.4.10"
  }
}

Code :

# The following iRule can be used if you are running a BIG-IP version prior to v11.

when RULE_INIT {
    set ::too_many_dns_parts 10
    set ::header_without_id [binary format S5 {0x8180 0x0001 0x0001 0x0000 0x0000}]
    #predefined fixed header 
    #      0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    #    |                      ID                       |
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    #    |QR|   Opcode  |AA|TC|RD|RA|   Z    |   RCODE   |
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    #    |                    QDCOUNT                    |
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    #    |                    ANCOUNT                    |
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    #    |                    NSCOUNT                    |
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    #    |                    ARCOUNT                    |
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    #
    #opcode = QUERY, rcode = NOERROR
    #header flags:  response, auth. answer, want recursion, recursion avail.
    #questions = 1,  answers = 1,  authority records = 0,  additional = 0
    set ::answerpart [binary format S6c4 {0xC00C 0x0001 0x0001 0x0000 0x0D1B 0x0004} {200 100 4 10}]
    #predefined Fixed Answer section 
    #Name: same as qestion
    #Type: Host address
    #Class: INET
    #Time to live: 55 minutes, 55seconds
    #Data length: 4
    #Addr: 200.100.4.10, you can modify your own IP address here
    #Data Structure
    #      0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    #    |                     Name                      |
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    #    |                  Answer Type                  |
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    #    |                    Class                      |
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    #    |              Time to live part 1              |
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    #    |              Time to live part 2              |
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    #    |               IP Address part 1               |
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    #    |               IP Address part 2               |
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
}

when CLIENT_ACCEPTED {

   #check QID, if query ID is 0, that means it's a illegal request.
   #qid 0 will consume server a lot of resource.
   binary scan [ string range [UDP::payload] 0 1 ] S sflags
   set qid  [expr $sflags & 0x00ff]
   #log "the request ID is $qid"
   if {$qid == 00  }{
     STATS::incr dns_request_stat dns_request
    }
}

when SERVER_DATA {
   #check rcode
   binary scan [ string range [UDP::payload] 2 3 ] S sflags
   set rcode  [expr $sflags & 0x000f]
   if {$rcode == 3  }{
      # skip the DNS header, jump to the QNAME part of first QUESTION
      # byte contains the first part length
      binary scan [string range [UDP::payload] 12 13 ] c foo
      # make the byte an unsigned integer
      set byte [expr $foo & 0xff]
      # initialize our posisition in the QNAME parsing and the text QNAME
      set offset 12
      # $i is a sanity check so this logic won't spin on invalid QNAMEs
      set i 0
      ############# /extract QNAME from QUESTION header #############
      while {$byte > 0 && $i < 10} {
         # grab a part and put it in our text QNAME section
         set offset [expr $offset + $byte + 1]
         # grab the length of the next part, and make it an unsigned integer
         set byte [string range [UDP::payload] $offset [expr $offset + 1]]
         binary scan $byte c foo   
         set byte [expr $foo & 0xff]
         incr i
      }
     # increment offset past the final part so it points at the QTYPE field
     incr offset
     ############# extract QTYPE from QUESTION header #############            
     # grab the next 2 bytes that represent the QTYPE
     binary scan [string range [UDP::payload] $offset [expr $offset + 2]] S qtype
     # see if the QTYPE is 0x0001 (TYPE_AAAA), if it's a A query, then replace the content
     if {$qtype == 0x0001} { 
       #Pack the respond packet
       #first a2 is id
       #second a* is predefined header without id
       #third a* is question part extract from return packet
       #fourth a* is predefined answer section
       #if we use UDP::respond here, we can let a LTM response DNS request just by rules
       #UDP::respond [binary format a2a*a*a* [string range [UDP::payload] 0 1] $::header_without_id [string range [UDP::payload] 12 [expr $offset+3]] $::answerpart]
       #UDP::drop
       UDP::payload replace 0 0 [binary format a2a*a*a* [string range [UDP::payload] 0 1] $::header_without_id [string range [UDP::payload] 12 [expr $offset+3]] $::answerpart]
     }
   }
}

# iRule Source

when RULE_INIT {
    set ::my_address {200 100 4 10}
    #Addr: 200.100.4.10, you can modify your own IP address here
    
    set ::header_without_id [binary format S5 {0x8180 0x0001 0x0001 0x0000 0x0000}]
    #predefined fixed header 
    #      0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    #    |                      ID                       |
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    #    |QR|   Opcode  |AA|TC|RD|RA|   Z    |   RCODE   |
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    #    |                    QDCOUNT                    |
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    #    |                    ANCOUNT                    |
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    #    |                    NSCOUNT                    |
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    #    |                    ARCOUNT                    |
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    #
    #opcode = QUERY, rcode = NOERROR
    #header flags:  response, auth. answer, want recursion, recursion avail.
    #questions = 1,  answers = 1,  authority records = 0,  additional = 0

    set ::answerpart [binary format S6c4 {0xC00C 0x0001 0x0001 0x0000 0x0D1B 0x0004} $::my_address]
    #predefined Fixed Answer section 
    #Name: same as qestion
    #Type: Host address
    #Class: INET
    #Time to live: 55 minutes, 55seconds
    #Data length: 4
    #IPv4 Addr: xxx.xxx.xxx.xxx
    #Data Structure
    #      0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    #    |                     Name                      |
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    #    |                  Answer Type                  |
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    #    |                    Class                      |
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    #    |              Time to live part 1              |
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    #    |              Time to live part 2              |
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    #    |               IP Address part 1               |
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    #    |               IP Address part 2               |
    #    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
}
when SERVER_DATA {
    #check rcode
    binary scan [UDP::payload] @2S sflags
    set rcode  [expr $sflags & 0x000f]
    #rcode = 3 Name Error (domain name referenced in the query does not exist.
    if {$rcode == 3  }{
        # skip the DNS header, jump to the QNAME part of first QUESTION
        # byte contains the first part length
        binary scan [UDP::payload] @12c foo
        # make the byte an unsigned integer
        set byte [expr ($foo+0x100)%0x100]
        # initialize our posisition in the QNAME parsing and the text QNAME
        set offset 12
        # $i is a sanity check so this logic won't spin on invalid QNAMEs
        set i 0
        ############# /extract QNAME from QUESTION header #############
        while {$byte > 0 && $i < 10} {
            # grab a part and put it in our text QNAME section
            set offset [expr $offset + $byte + 1]
            # grab the length of the next part, and make it an unsigned integer
            binary scan [UDP::payload] @${offset}c foo
            set byte [expr ($foo+0x100)%0x100 ]
            incr i
        }
        # increment offset past the final part so it points at the QTYPE field
        incr offset
        ############# extract QTYPE from QUESTION header #############            
        # grab the next 2 bytes that represent the QTYPE
        binary scan [UDP::payload] @${offset}S qtype
        # see if the QTYPE is 0x0001 (TYPE_AAAA), if it's a A query, then replace the content
        if {$qtype == 0x0001} { 
            #Pack the respond packet
            #keep original query id
            #replace header with predefined header without id (answer part and remove NS/AR part
            #replace the rest of packet after query section with predefined answer section
            UDP::payload replace 2 10 $::header_without_id
            incr offset 4
            UDP::payload replace $offset [expr [UDP::payload length] - $offset ] $::answerpart
        }
    }
}
Published Mar 17, 2015
Version 1.0
No CommentsBe the first to comment