Back in October, I attended a Security B-Sides event in Jefferson City (review here). One of the presenters (@bethayoung) talked about poisoning the internal DNS intentionally for known purveyors of all things bad. I indicated in my write-up that I’d be detailing an F5-based solution, and whereas a few weeks has turned into a couple months, well, here we are. As much as I had hoped to get it all together on my own, F5er Hugh O’Donnell beat me to it, and did a fantastic job. F5er Lee Orrick also contributed to the solution and I’ll have more from him in a future article.

Conceptual Overview

Before jumping into the nuts and bolts, I’d like to describe the solution. First, consider normal operation: Joe Anonymous is surfing and hits a popular page that has been compromised. He hits a link for a cute video about puppies and rainbows and NOT SO FAST MY FRIEND! Instead of said cute puppies and rainbows video, he ends up with a nasty case of malware and his friendly neighborhood IT staff gets to spend some time remediating the damage—if it’s caught at all. See, DNS is if not the backbone of the internet, at least several of the vertebrae.  And it does its job very well. Asked and answered. Done. If you hit a link with a malicious domain, there’s a very very good chance your DNS server will have no safeguards in place, it’ll answer away. This is what a blackhole DNS solution is configured to overcome. The networking folks in the audience will be familiar with blackhole routing, and this is really no different a concept. When a user makes a query, the service inspects the destination, and if it matches a list of well known badness, it returns an address of an internal site where remediation or at least notification can take place. In either event, the request is not hitting the malicious destination, which protects user and organization. See Figure 1 for the flow detail.

Building the Datagroup

As with iFiles in v11.1, datagroups can also be imported via the GUI and then referenced similarly. To import your blacklisted domains (there’s a big list here:, make sure your text editor is set for line feed terminator only (CR-LF won’t work) and use this format for each entry:

“” := “harmful”,

“” := “zeusv2”,

The first field is the domain, and the second field is a type description. The first will match your traffic, the second is strictly for classification purposes and can be edited as necessary.

Intercepting the DNS Requests

This solution can be implemented with LTM or GTM, though if the latter, the iRule will still need to be attached to the virtual server associated with the wideIP instead of the wideIP itself. In this article, I’ll implement the LTM-based solution. As I’ll be utilizing the new DNS:: commands, a DNS profile will need to be attached to the virtual server as well as the iRule below. Note that the blackhole class (named appropriately Blackhole_Class in the iRule below) should be present on the system for this solution to work.

   1: # Author:  Hugh O'Donnell, F5 Consulting
   2: when RULE_INIT {
   3:     # Set IPV4 address that is returned for Blackhole matches for A records
   4:     set static::blackhole_reply_IPV4 ""
   5:     # Set IPV6 address that is returned for Blackhole matches for AAAA records
   6:     set static::blackhole_reply_IPV6 "2001:19b8:101:2::f5f5:1d"
   7:     # Set TTL used for all Blackhole replies
   8:     set static::blackhole_ttl "300"
   9: }
  11: when DNS_REQUEST {
  12:     # debugging statement see all questions and request details
  13:   # log -noname local0. "Client: [IP::client_addr] Question:[DNS::question name] Type:[DNS::question type] Class:[DNS::question class] Origin:[DNS::origin]"
  15:     # Blackhole_Match is used to track when a Query matches the blackhole list
  16:     # Ensure it is always set to 0 or false at beginning of the DNS request
  17:     set Blackhole_Match 0
  19:     # Blackhole_Type is used to track why this FQDN was added to the Blackhole_Class
  20:     set Blackhole_Type ""
  22:     # When the FQDN from the DNS Query is checked against the Blackhole class, the FQDN must start with a
  23:     # period.  This ensures we match a FQDN and all names to the left of it.  This prevents against
  24:     # malware that dynamically prepends characters to the domain name in order to bypass exact matches
  25:     if {!([DNS::question name] == ".")} {
  26:         set fqdn_name .[DNS::question name]
  27:     }
  29:     if { [class match $fqdn_name ends_with Blackhole_Class] } {
  30:         # Client made a DNS request for a Blackhole site.
  31:         set Blackhole_Match 1
  32:         set Blackhole_Type [class match -value $fqdn_name ends_with Blackhole_Class ]
  34:         # Prevent processing by GTM, DNS Express, BIND and GTM Listener's pool. 
  35:         # Want to ensure we don't request a prohibited site and allow their server to identify or track the GTM source IP.
  36:         DNS::return
  37:     }    
  38: }
  40: when DNS_RESPONSE {
  41:     # debugging statement to see all questions and request details
  42:     # log -noname local0. "Request: $fqdn_name Answer: [DNS::answer] Origin:[DNS::origin] Status: [DNS::header rcode] Flags: RD [DNS::header rd] RA [DNS::header ra]"
  44:     if { $Blackhole_Match } {
  45:         # This DNS request was for a Blackhole FQDN. Take different actions based on the request type.
  46:         switch [DNS::question type] {
  47:             "A"     {
  48:                     # Clear out any DNS responses and insert the custom response.  RA header = recursive answer
  49:                     DNS::answer clear
  50:                     DNS::answer insert "[DNS::question name]. $static::blackhole_ttl [DNS::question class] [DNS::question type] $static::blackhole_reply_IPV4"
  51:                     DNS::header ra "1"
  53:                     # log example:  Apr  3 14:54:23 local/tmm info tmm[4694]:
  54:                     #     Blackhole: requested query type: A class IN A-response:
  55:                     log -noname local0. "Blackhole: [IP::client_addr]#[UDP::client_port] requested [DNS::question name] query type: [DNS::question type] class [DNS::question class] A-response: $static::blackhole_reply_IPV4 BH type: $Blackhole_Type"
  56:                     }
  57:             "AAAA"     {
  58:                     # Clear out any DNS responses and insert the custom response.  RA header = recursive answer
  59:                     DNS::answer clear
  60:                     DNS::answer insert "[DNS::question name]. $static::blackhole_ttl [DNS::question class] [DNS::question type] $static::blackhole_reply_IPV6"
  61:                     DNS::header ra "1"
  63:                     # log example:  Apr  3 14:54:23 local/tmm info tmm[4694]:
  64:                     #     Blackhole: requested query type: A class IN AAAA-response: 2001:19b8:101:2::f5f5:1d
  65:                     log -noname local0. "Blackhole: [IP::client_addr]#[UDP::client_port] requested [DNS::question name] query type: [DNS::question type] class [DNS::question class] AAAA-response: $static::blackhole_reply_IPV6 BH type: $Blackhole_Type"
  66:                     }
  67:             default {
  68:                     # For other record types, e.g. MX, NS, TXT, etc, provide a blank NOERROR response
  69:                     DNS::last_act reject
  71:                     # log example:  Apr  3 14:54:23 local/tmm info tmm[4694]:
  72:                     #     Blackhole: requested query type: A class IN unable to respond
  73:                     log -noname local0. "Blackhole: [IP::client_addr]#[UDP::client_port] requested [DNS::question name] query type: [DNS::question type] class [DNS::question class] unable to respond  BH type: $Blackhole_Type"
  74:                     }
  75:         }
  76:     }
  77: }

This iRule handles the DNS request, responding on behalf of GTM or any DNS servers being load balanced by LTM. And since we’re handling the blackhole site, we can serve that up as well from an iRule on an HTTP virtual server.

Serving the Remediation Page

The remediation page can be as simple as a text message indicating malware, or it can be a little more complex to show the category of the problem site as well as provide some contact information. The iRule below is an example of the latter.

   1: # Author: Hugh O’Donnell, F5 Consulting
   3: when HTTP_REQUEST {
   5:     # the static HTML pages include the logo that is referenced in HTML as corp-logo.gif
   6:     # intercept requests for this and reply with the image that is stored in an iFile defined in RULE_INIT below
   7:     if {[HTTP::uri] ends_with "/_maintenance-page/corp-logo.png" } {
   8:         # Present
   9:        HTTP::respond 200 content $static::corp_logo
  11:     } else {
  12:         # Request for Blackhole webpage.  Identify what type of block was in place
  13:         switch -glob [class match -value ".[HTTP::host]" ends_with Blackhole_Class ] {
  14:                 "virus"     { set block_reason "Virus site" }
  15:                 "phishing"     { set block_reason "Phishing site" }
  16:                 "generic"     { set block_reason "Unacceptable Usage" }
  17:                 default     { set block_reason "Denied Per Policy - Other Sites" }
  18:         }
  20:         # Log details about the blackhole request to the remote syslog server
  21:         log -noname local0. "Blackhole: From [IP::client_addr]:[TCP::client_port] \
  22:           to [IP::local_addr]:[TCP::local_port], [HTTP::request_num], \
  23:           [HTTP::method],[HTTP::uri],[HTTP::version], [HTTP::host],  [HTTP::header value Referer], \
  24:           [HTTP::header User-Agent], [HTTP::header names],[HTTP::cookie names], BH category: $block_reason,"
  26:         # Send an HTML page to the user.  The page is defined in the RULE_INIT event below
  27:         HTTP::respond 200 content "$static::block_page [HTTP::host][HTTP::uri] $static::after_url $block_reason $static::after_block_reason "
  28:     }   
  29: }
  32: when RULE_INIT {
  33:     # load the logo that was stored as an iFile
  34:     set static::corp_logo [ifile get "/Common/f5ball"]
  36:     # Beginning of the block page
  37:     set static::block_page "
  38:         <html lang=\"en_US\">
  39:         <head>
  40:         <title>Web Access Denied - Enterprise Network Operations Center</title>
  41:         <meta http-equiv=\"Content-Type\" content=\"text/html; charset=us-ascii\">
  42:         <meta http-equiv=\"CACHE-CONTROL\" content=\"NO-CACHE\">
  43:         <meta http-equiv=\"PRAGMA\" content=\"NO-CACHE\">
  44:         <meta http-equiv=\"EXPIRES\" content=\"Mon, 22 Jul 2002 11:12:01 GMT\">
  45:         <style>
  46:         <!--
  47:         .mainbody {
  48:             background-color: #C0C0C0;
  49:             color: #000000;
  50:             font-family: Verdana, Geneva, sans-serif;
  51:             font-size: 12px;
  52:             margin: 0px;
  53:             padding: 20px 0px 20px 0px;
  54:             position: relative;
  55:             text-align: center;
  56:             width: 100%;
  57:         }
  58:         .bdywrpr {
  59:             width:996px;
  60:             height:auto;
  61:             text-align:left;
  62:             margin:0 auto; 
  63:             z-index:1;
  64:             position: relative;
  65:         }
  66:         #banner-wrapper {
  67:             width: 950px;
  68:             padding: 0px;
  69:             margin: 0px;
  70:             overflow:hidden;
  71:             background-color: #FFFFFF;
  72:             background-repeat: no-repeat;
  73:         }
  74:         #banner-image {
  75:             float: left;
  76:             margin-left: auto;
  77:             margin-right: auto;
  78:             padding: 3px 0px 2px 7px;
  79:             width: 950px;
  80:         }
  81:         #textbody {
  82:             background-color: #FFFFFF;
  83:             color: #000000;
  84:             font-family: Verdana, Geneva, sans-serif;
  85:             font-size: 13px;
  86:             width: 950px;
  87:             padding:0px;
  88:             text-align:justify;
  89:             margin: 0px;
  91:         }
  92:         -->
  93:         </style>
  94:         </head>
  95:         <body class=\"mainbody\">
  96:         <div class=\"bdywrpr\">
  98:                    <div id=\"banner-wrapper\">
  99:                         <!-- BANNER -->
 100:                         <div id=\"banner-image\">
 101:                              <center><img src=\"/_maintenance-page/corp-logo.png\" alt=\"Enterprise Network Operations Center\"></center>
 102:                         </div>
 103:                     </div>
 104:                     <div id=\"textbody\">
 105:                     <table border=\"0\" cellpadding=\"40\"><tr><td>
 106:                         <center><p style=\"font-size:18px\"><b>Access has been denied.<br><br> URL: "
 108:     set static::after_url "</p></center></b></font> <br>
 109:                     Your request was denied because it is blacklisted in DNS. <br><br>
 110:                     Blacklist category: "
 112:     set static::after_block_reason "<br>
 113:                     <p>
 114:                     The Internet Gateways are for official use only. Misuse violates policy.
 115:                     If you believe that this site is categorized incorrectly, and that you have a valid business
 116:                     reason for access to this site please contact your manager for approval
 117:                     and the Enterprise Network Operations Center via
 118:                     <br><br>
 119:                     E-mail: <a href=\"\"></a> <br><br>
 120:                     Please use the Web Access Request Form and include a business justification.
 121:                     &nbsp;
 122:                     Only e-mail that originates from valid internal e-mail addresses will be processed. 
 123:                     If you do not have a valid e-mail address, your manager will need to submit a request on your behalf.
 124:                     </center>
 125:                     <p>
 126:                     <font size=-1><i>Generated by</i></font>
 127:                 </td></tr></table>
 128:                 </div>
 129:             </div>
 130:             </body> </html>
 131:         "
 132: }

Note that the remediation page references an iFile for a logo. For details on configuring iFiles, please reference my article on iFiles. Also note that in addition to the client getting a heads-up notification of malfeasance, the visit is logged so other processes, individuals can act on the information.

The Results

First, our DNS query and response. Rather than test out a real well-known bad site, I added to my blacklist so if I forgot a step and leaked through to the real site I wouldn’t compromise anything. The response from my DNS virtual server is shown in Figure 2 below.

You can see that the address matches the address set in the iRule as our blackhole IPv4 address. Also, the log information from that DNS query:

Dec 28 15:35:08 tmm info tmm[6883]: Blackhole: requested query type: A class IN A-response: BH type: sports

Next, the resulting remediation page in my browser (Figure 3):

And finally, the log entry from the HTTP request:

Dec 28 15:35:08 tmm info tmm[6883]: Blackhole: From to, 1, GET,/,1.1,, , Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.63 Safari/535.7, Host Connection User-Agent Accept Accept-Encoding Accept-Language Accept-Charset,, BH category: Denied Per Policy - Other Sites,


This is a wicked application of iRules with new DNS and file handling features delivered in v11.1. If you wanted to take it even further, you could use sideband connections and reference an external list instead of a datagroup that will need constant refreshing. The GTM version of this solution is documented in the wiki: If you’re curious about the DNS commands used in the iRule above, I’ll be discussing them in my next tech tip, so check back soon!


Update: For the LTM solution presented above, the DNS Services module or the GTM module is required to be licensed.