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
Updated Jun 06, 2023
Version 2.0

Was this article helpful?

No CommentsBe the first to comment