DNS lite with topology selection
Problem this snippet solves:
19/12/2017 : add lowercase conversion of the requested name to support case insensitive request.
BIGIP DNS is a really great product to load balance DNS requests to the best Datacenter. This codes is not a solution to replace the DNS product but to provide a solution to manage small DNS needs.
This code isn't full RFC compliant but provide an internal DNS server supporting only A and AAAA query types. The goal of this code was to permit to manage datacenter HA for a single application (exchange in my case).
This code search for the preferred network in a data group based on the DNS client IP source.
Then, If the request is
www.example.com
and there is a LTM pool named p_gtm_lite_www.example.com
(with virtual servers as members), the active pool members are compared to the preferred network.
If no pool member matches the preferred network, the last pool member is selected (no LB is enabled)
If the pool does not exist, a NXDOMAIN is answered.
How to use this snippet:
create a Datagroup named
gtm_lite_topology
ltm data-group internal gtm_lite_topology { records { 0.0.0.0 { data 192.168.2.0/24 } 192.168.1.0/24 { data 192.168.1.0/24 } } type ip }
create a LTM pool with VS as members. don't forget to enable LTM monitor.
Note: The pool name must be lowercase.
ltm pool p_gtm_lite_www.example.com { members { vs2:https { address 192.168.2.237 } vs1:https { address 192.168.1.237 } } }
change domain name in variable
static::domain
Change the max dns members in response in variable
static::dns_max_response
Code :
when RULE_INIT { set static::DNS_TTL 30 set static::dns_max_response 2 } when CLIENT_ACCEPTED { # Initiate response flags to ANSWER; 16 bits with first set to 1 # qr(1) opcode(0000) AA(1) TC(0) RD(0) RA(0) Z(000) RCODE(0000) ################################################### # DNS RCODE Assignment as defined in RFC 2929 # RCODE Name Description Reference # 0 NoError No Error [RFC 1035] # 1 FormErr Format Error [RFC 1035] # 2 ServFail Server Failure [RFC 1035] # 3 NXDomain Non-Existent Domain [RFC 1035] # 4 NotImp Not Implemented [RFC 1035] # 5 Refused Query Refused [RFC 1035] # 6 YXDomain Name Exists when it should not [RFC 2136] # 7 YXRRSet RR Set Exists when it should not [RFC 2136] # 8 NXRRSet RR Set that should exist does not [RFC 2136] # 9 NotAuth Server Not Authoritative for zone [RFC 2136] # 10 NotZone Name not contained in zone [RFC 2136] # 16 BADVERS Bad OPT Version [RFC 2671] # 16 BADSIG TSIG Signature Failure [RFC 2845] # 17 BADKEY Key not recognized [RFC 2845] # 18 BADTIME Signature out of time window [RFC 2845] # 19 BADMODE Bad TKEY Mode [RFC 2930] # 20 BADNAME Duplicate key name [RFC 2930] # 21 BADALG Algorithm not supported [RFC 2930] set RESPONSE_FLAGS "1000010000000000" # Initiate response resources. each resource type is created in a list allowing multiple resources of same type. set QUESTION_LIST [list]; set ANSWER_LIST [list]; set AUTHORITY_LIST [list]; set ADDITIONAL_LIST [list] # Decode UDP DNS request. If all variables are not set, drop the request if {[binary scan [UDP::payload] SB16SSSSH* ID REQUEST_FLAGS QDCOUNT ANCOUNT NSCOUNT ARCOUNT RESOURCES] < 7} { drop; return } # If ANSWER resource exists or different than 1 question, drop the request if {$ANCOUNT > 0 || $QDCOUNT != 1} { # change RCODE to value 5 (0101) : Refused set RESPONSE_FLAGS [string replace $RESPONSE_FLAGS 12 15 "0101"] } else { # According to RFC, a security-aware name server MUST copy the CD bit from a query into the corresponding response set RESPONSE_FLAGS [string replace $RESPONSE_FLAGS 7 7 [string index $REQUEST_FLAGS 7]] set RESPONSE_FLAGS [string replace $RESPONSE_FLAGS 11 11 [string index $REQUEST_FLAGS 11]] ################################################### ###### extract query name, type and class from resources # DNS Question format as defined in RFC 1035 # - NAME : 255 octets or less containing a sequence of labels (63 octets or less). # - TYPE: two octets containing one of the RR TYPE codes. # - CLASS: two octets containing one of the RR CLASS codes. set QNAME_LIST {} set index 1 set BIN_RESOURCES [binary format H* $RESOURCES] # Read the first label length binary scan $BIN_RESOURCES c label_length while {$index < [string length $BIN_RESOURCES]} { # Read label and the next label length binary scan $BIN_RESOURCES @${index}A${label_length}c label_value next_length lappend QNAME_LIST "$label_value" set index [expr {$index + 1 + $label_length}] set label_length $next_length if {$label_length == 0} { binary scan $BIN_RESOURCES @${index}SS TYPE CLASS #binary scan $BIN_RESOURCES H[expr {$index * 2}] QNAME_HEX # pointer C00C means 12 bytes from ID --> start of Question label set QNAME_HEX "C00C" # extract Question record to insert it in the response binary scan $BIN_RESOURCES H[expr {$index * 2 + 8}] QUESTION_HEX # append Question record in Question RR list lappend QUESTION_LIST $QUESTION_HEX # Join all labels with dot separator. set QNAME [string tolower [join $QNAME_LIST "."]] break } } unset index if {$TYPE == 12 && $QNAME ends_with ".in-addr.arpa"} { if {[scan $QNAME "%d.%d.%d.%d" d c b a] == 4 && [IP::addr "$a.$b.$c.$d" equals [IP::local_addr]] } { set data "" foreach label [split [string tolower [info hostname]] "."] { binary scan [binary format cA* [string length $label] $label] H* tmp append data $tmp } append data "00" log local0. $data binary scan [binary format H*SSISH* $QNAME_HEX ${TYPE} ${CLASS} ${static::DNS_TTL} [expr {[string length $data] / 2}] ${data}] H* HEX_RESOURCE_RECORD ################################################### # Add the resource record to the Answer RR list lappend ANSWER_LIST $HEX_RESOURCE_RECORD log local0. "[IP::client_addr] / $QNAME : [info hostname] added to list" } } elseif {[catch { set pool_members [active_members -list "p_gtm_lite_$QNAME"] }]} { log local0. "error $QNAME record not exists" # change RCODE to value 3 (0011) : NXDomain set RESPONSE_FLAGS [string replace $RESPONSE_FLAGS 12 15 "0011"] } else { #retreive prefered network from Topology datagroup set PREFERED_NETWORK [class match -value [IP::client_addr] equals gtm_lite_topology] set member_addr_list [list] # sort pool member list to put preferred member first foreach pmember $pool_members { if {$PREFERED_NETWORK equals "" || [IP::addr [set pmember_addr [lindex $pmember 0]] equals $PREFERED_NETWORK]} { set member_addr_list [linsert $member_addr_list 0 $pmember_addr] } else { lappend member_addr_list $pmember_addr } } ################################################### # Create answer(s) based on the requested record type # # DNS Resource record format as defined in RFC 1035 # - NAME : 255 octets or less containing a sequence of labels (63 octets or less) or offset pointer. pointer C00C means 12 bytes from ID --> start of Question label # - TYPE: two octets containing one of the RR TYPE codes. # - CLASS: two octets containing one of the RR CLASS codes. # - TTL: 32 bit signed integer # - RDLENGTH: unsigned 16 bit integer that specifies the length in octets of the RDATA field # - RDATA: a variable length string of octets that describes the resource. The format of this information varies according to the TYPE and CLASS of the resource record. switch $TYPE { 1 { # If query type is A foreach item $member_addr_list { if {[llength $ANSWER_LIST] < ${static::dns_max_response}} { #convert IPv4 to hexadecimal if {[scan $item "%d.%d.%d.%d" a b c d] == 4} { set data [format %02x%02x%02x%02x $a $b $c $d] binary scan [binary format H*SSISH* $QNAME_HEX ${TYPE} ${CLASS} ${static::DNS_TTL} [expr {[string length $data] / 2}] ${data}] H* HEX_RESOURCE_RECORD ################################################### # Add the resource record to the Answer RR list lappend ANSWER_LIST $HEX_RESOURCE_RECORD log local0. "[IP::client_addr] / $QNAME : $item added to list" } } else {break} } } 28 { # If query type is AAAA foreach item $member_addr_list { if {[llength $ANSWER_LIST] < ${static::dns_max_response}} { #convert IPv6 to hexadecimal (add missing 0, remove colons) if {$item contains ":"} { if {$item contains "::"} { set ipv6_begin "" foreach val [split [getfield $item "::" 1] ":"] {append ipv6_begin [format %04x 0x$val]} set ipv6_end "" foreach val [split [getfield $item "::" 2] ":"] { append ipv6_end [format %04x 0x$val]} set data "$ipv6_begin[format %0[expr {32 - [string length $ipv6_begin$ipv6_end]}]x 0]$ipv6_end" } else { set data "" foreach val [split $item ":"] {append data [format %04x 0x$val]} } binary scan [binary format H*SSISH* $QNAME_HEX ${TYPE} ${CLASS} ${static::DNS_TTL} [expr {[string length $data] / 2}] ${data}] H* HEX_RESOURCE_RECORD ################################################### # Add the resource record to the Answer RR list lappend ANSWER_LIST $HEX_RESOURCE_RECORD log local0. "[IP::client_addr] / $QNAME : $item added to list" } } else {break} } } } } } ################################################### # DNS Message format as defined in RFC 1035 # - Header : Header section # - ID: A 16 bit identifier assigned by the program that generates any kind of query. # - Flags: Flags containing: #- QR: 1 bit : A one bit field that specifies whether this message is a query (0), or a response (1). #- Opcode: 4 bits : A four bit field that specifies kind of query in this message #- AA: 1 bit : Authoritative Answer #- TC: 1 bit : TrunCation - specifies that this message was truncated due to length greater than that permitted on the transmission channel. #- RD: 1 bit : Recursion Desired #- RA: 1 bit : Recursion Available #- Z: 3 bits : Reserved for future use. Must be zero in all queries and responses. #- RCODE : 4 bits : Response code - this 4 bit field is set as part of responses. 0 for NOERROR # - QDCOUNT: unsigned 16 bit integer specifying the number of entries in the question section. # - ANCOUNT: unsigned 16 bit integer specifying the number of entries in the answer section. # - NSCOUNT: unsigned 16 bit integer specifying the number of entries in the authority records section. # - ARCOUNT: unsigned 16 bit integer specifying the number of entries in the additional records section. # - Question: a possibly empty list of concatenated resource records (RRs) # - Answer: a possibly empty list of concatenated resource records (RRs) # - Authority: a possibly empty list of concatenated resource records (RRs) # - Additional: a possibly empty list of concatenated resource records (RRs) drop UDP::respond [binary format SB16SSSSH* ${ID} $RESPONSE_FLAGS [llength $QUESTION_LIST] [llength $ANSWER_LIST] [llength $AUTHORITY_LIST] [llength $ADDITIONAL_LIST] [join ${QUESTION_LIST} ""][join ${ANSWER_LIST} ""][join ${AUTHORITY_LIST} ""][join ${ADDITIONAL_LIST} ""]] }
Tested this on version:
11.6