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
}
}
}