DNS Profile Benefits in iRules

 

 

I released an article a while back on the DNS services architecture now built in to BIG-IP, as well as a solution article that showed some fancy DNS tricks utilizing the architecture to black hole malicious DNS requests. What might be lost in those articles is the difference maker the dns profile makes in using iRules to return DNS responses. I was working on a little project earlier this week and the VM I am hosting requires a single DNS response to a single question. The problem is that I don't have the particular fqdn defined in an external or internal name server. Adding the fqdn to either is problematic:

  1. Adding the FQDN to the external name server would require adding an internal view to bind, which adds risk and complexity.
  2. Adding the FQDN to the internal name server would require adding external zones to my internal server, which adds unnecessary complexity.

So as I wasn't going down either of those roads...I had to find an alternate solution. Thankfully, I have BIG-IP VE at my disposal, and therefore, iRules. The DNS profile exposes in iRules the DNS:: namespace, and with it, native decodes for all the fields in requests/responses. The iRule, with the DNS namespace, is trivial:

when DNS_REQUEST {
  if { [IP::addr [IP::remote_addr] equals 192.168.1.0/24] && ([DNS::question name] equals "www.mytest.com") } {
    DNS::answer insert "[DNS::question name]. 111 [DNS::question class] [DNS::question type] 192.168.1.200"
    DNS::return
  } else ( discard }
}

However, after trying to save the iRule, I realized I'm not licensed for dns services on my BIG-IP VE, so that path wouldn't work. So I took a packet capture of some local dns traffic on my desktop and started mapping the fields and preparing to settle in for some serious binary scan/format work, but then remembered there were already some iRules out in the codeshare that I though might get me started. Natty76's Fast DNS 2 seemed to fit the bill. So with just a little customization, I was up and running with no issues. But notice the amount of work required (both by author and by system resources) to make this happen when compared with the above iRule.

when RULE_INIT priority 1 {
    # Domain Name = www mytest com
    set static::domain "www.mytest.com"    
    # IP address in answer section (type A)
    set static::answer_string "192.168.1.200"
}
when RULE_INIT {
    # Header generation (in hexadecimal)
    # qr(1) opcode(0000) AA(1) TC(0) RD(1) RA(1) Z(000) RCODE(0000)
    set static::header "8580"
    
    # 1 question, X answer, 0 NS, 0 Addition
    set static::answer_record [format %04x [llength $static::answer_string]]
    set static::header "${static::header}0001${static::answer_record}00000000"
    
    # generate domain binary string
    set static::domainhex ""
    foreach static::d [split $static::domain "."] {
        set static::l [string length $static::d]
        scan $static::l %d static::h
        append static::domainhex [format %02x $static::h]
        foreach static::n [split $static::d ""] {
            scan $static::n %c static::h
            append static::domainhex [format %02x $static::h]
        }
    }
    set static::domainbin [binary format H* $static::domainhex]
    append static::domainhex 00

    set static::answerhead $static::domainhex
    # Type = A
    set static::answerhead "${static::answerhead}0001"
    # Class = IN
    set static::answerhead "${static::answerhead}0001"
    # TTL = 1 day
    set static::answerhead "${static::answerhead}00015180"
    # Data length = 4
    set static::answerhead "${static::answerhead}0004"

    set static::answer ""
    foreach static::a $static::answer_string {
        scan $static::a "%d.%d.%d.%d" a b c d
        append static::answer "${static::answerhead}[format %02x%02x%02x%02x $a $b $c $d]"
    }
}
when CLIENT_DATA {
  if { [IP::addr [IP::client_addr] equals 192.168.1.0/22] } {
    
    binary scan [UDP::payload] H4@12A*@12H* id dname question
    set dname [string tolower [getfield $dname \x00 1 ] ]
    
    switch -glob $dname \
      $static::domainbin {
        #log local0. "match"
        set hex ${id}${static::header}${question}${static::answer}
        set payload [binary format H* $hex ]
        # to drop only a packet and keep UDP connection, use UDP::drop
        drop
        UDP::respond $payload
      } \
      default {
        #log local0. "does not match"
      }
  } else { discard }
}

No native decode means you have to do all the decoding work of the protocol yourself. I don't get to share "from the trenches" as much as I used to, but this was too good a demonstration to pass up.

Published May 08, 2013
Version 1.0

Was this article helpful?

3 Comments

  • Hi,

     

    I found this article too late... I wrote my own code before :-( ! The picture made me laugh because it remembered me trying to understand the DNS encoding logic in RFC..

     

    I had a customer who used the DNS-Lite license for DC HA requirements for only one Application : Exchange.

     

    there are only 4 DNS servers requesting to this record.

     

    The rate limite of this license was enough for this need. There is 1 dns request per minute for the only one record.

     

    Now the customer had to upgrade to 12.1.2 Appliances so I had to find a solution before to reactivate the license. the DNS license is too expensive for this little need.

     

    I first wrote an irule like yours which worked when I requested from dig and nslookup utility to the F5 but I have encountered an issue when the DNS server includes a "additional record".

     

    This record is appended after the question, so it is included in the ${question} variable. As you insert the answer after it, there is a mismatch in the response.

     

    the customer's AD 2012 always included an additional record. so the answer was rejected and the DNS cache was cleared... Exchange was not working anymore. oops!

     

    You can look at my code DNS lite with topology selection witch decode the request and encode the response based on pool member availability.

     

  • AP's avatar
    AP
    Icon for Nimbostratus rankNimbostratus

    Hi, In case this is useful to others (and also to validate my understanding), I found that with the following code:

     

    binary scan [UDP::payload] H4@12A*@12H* id dname question
    set dname [string tolower [getfield $dname \x00 1 ] ]
    

    ... a label of more than 15 characters resulted in the $dname including a meta-character at the start of the dname. E.g. a hostname with 45 characters resulted in a label length of hex "2d", which the iRule interpreted as a "-". A meta-character leading the dname string results in unpredictable behavior. In the case of the "-", the switch command errors out interpreting it as a command option.

     

    To resolve the issue I simply skipped the length count by change the binary scan offset for dname to 13 bytes.

     

    E.g.:

     

    binary scan [UDP::payload] @13A* dname
    

    As far as I can tell this shouldn't cause any issues, at least for the hostname portion, since we're just skipping the length count.

     

    The best solution would be to by DNS service license though ;).

     

    Also, for those want to do the same for TCP, you need to use TCP::collect and TCP::release and add 2 to the binary scan offset.

     

  • AP's avatar
    AP
    Icon for Nimbostratus rankNimbostratus

    Actually, looking at Stanislas' DNS lite code, it seems he's actually done the binary scan in a more RFC friendly manner, so best to use that as a base. In my case I'm just doing External DNS load balancing with the exception of a few zones that should be load balanced to internal DNS servers.