Learn F5 Technologies, Get Answers & Share Community Solutions Join DevCentral

Filter by:
  • Solution
  • Technology
Answers

Selective HTTPS proxy with SNI

It was recently requested that we pursue proxying outbound HTTPS calls made to various sites that we don't own and subsequently don't have keys for. Some of these require client certificates which we would like to manage centrally on the LTM. In running through the various different ways to accomplish this, I had a thought of how to do this with one VIP dynamically.

To do this I setup a data group that contained a list of hostnames I want to proxy HTTPS connections to along with the name of the corresponding client SSL profile. For those client profiles I generated cert/key pairs from an trusted internal CA. Then I created a server ssl profile for each of those hostnames. On the wildcard port 443 standard VIP I am using the default gateway IP as my node in the default pool, a oneconnect profile, and have port and address translation turned off. Then, I have a generic client and server ssl profile attached with the default setting and cert. Lastly, I have the iRule below attached. Much of the guts of this came from Colin Walkers article about how to accomplish SNI before it was natively available in BigIP, https://devcentral.f5.com/articles/multiple-certs-one-vip-tls-server-name-indication-via-irules.

The goal here is to terminate the connection on the client side with a key we own so we can passively decrypt the traffic else where and then re-encrypt the traffic validating the server side certificate and presenting the necessary client certificate to that server. For traffic not "interesting" or unknown(not in the data group) we want the HTTPS traffic to passthrough the LTM with no ssl profiles. This seems to be working, so I figured I would share with the community, I am open to comments, questions, and concerns about this approach and wonder if anyone can thinking of any gotchas I'm over looking.

when CLIENT_ACCEPTED {
    if { [PROFILE::exists clientssl] } {
        # We have a clientssl profile attached to this VIP but we need
        # to find an SNI record in the client handshake. To do so, we'll
        # disable SSL processing and collect the initial TCP payload.

        set detect_handshake 1
        SSL::disable
        TCP::collect
    }
    else {
        # No clientssl profile means we're not going to work.

        set detect_handshake 0
    }
}

when CLIENT_DATA {
    if { ($detect_handshake) } {
        # If we're in a handshake detection, look for an SSL/TLS header.

        binary scan [TCP::payload] cSS tls_xacttype tls_version tls_recordlen

        # TLS is the only thing we want to process because it's the only
        # version that allows the servername extension to be present. When we
        # find a supported TLS version, we'll check to make sure we're getting
        # only a Client Hello transaction -- those are the only ones we can pull
        # the servername from prior to connection establishment.

        switch -- $tls_version {
            "769" -
            "770" -
            "771" {
                if { ($tls_xacttype == 22) } {
                    binary scan [TCP::payload] @5c tls_action
                    if { not (($tls_action == 1) && ([TCP::payload length] > $tls_recordlen)) } {
                        set detect_handshake 0
                    }
                }
            }
            default {
                set detect_handshake 0
            }
        }

        if { ($detect_handshake) } {
            # If we made it this far, we're still processing a TLS client hello.
            #
            # Skip the TLS header (43 bytes in) and process the record body. For TLS/1.0 we
            # expect this to contain only the session ID, cipher list, and compression
            # list. All but the cipher list will be null since we're handling a new transaction
            # (client hello) here. We have to determine how far out to parse the initial record
            # so we can find the TLS extensions if they exist.

            set record_offset 43
            binary scan [TCP::payload] @${record_offset}c tls_sessidlen
            set record_offset [expr {$record_offset + 1 + $tls_sessidlen}]
            binary scan [TCP::payload] @${record_offset}S tls_ciphlen
            set record_offset [expr {$record_offset + 2 + $tls_ciphlen}]
            binary scan [TCP::payload] @${record_offset}c tls_complen
            set record_offset [expr {$record_offset + 1 + $tls_complen}]

            # If we're in TLS and we've not parsed all the payload in the record
            # at this point, then we have TLS extensions to process. We will detect
            # the TLS extension package and parse each record individually.

            if { ([TCP::payload length] > $record_offset) } {
                binary scan [TCP::payload] @${record_offset}S tls_extenlen
                set record_offset [expr {$record_offset + 2}]
                binary scan [TCP::payload] @${record_offset}a* tls_extensions

                # Loop through the TLS extension data looking for a type 00 extension
                # record. This is the IANA code for server_name in the TLS transaction.

                for { set x 0 } { $x < $tls_extenlen } { incr x 4 } {
                    set start [expr {$x}]
                    binary scan $tls_extensions @${start}SS etype elen
                    if { ($etype == "00") } {   
                        # A servername record is present. Pull this value out of the packet data
                        # and save it for later use. We start 9 bytes into the record to bypass
                        # type, length, and SNI encoding header (which is itself 5 bytes long), and
                        # capture the servername text (minus the header).

                        set grabstart [expr {$start + 9}]
                        set grabend [expr {$elen - 5}]
                        binary scan $tls_extensions @${grabstart}A${grabend} tls_servername
                        set start [expr {$start + $elen}]
                    }
                    else {
                        # Bypass all other TLS extensions.

                        set start [expr {$start + $elen}]
                    }
                    set x $start
                }

                # Check to see whether we got a servername indication from TLS. If so,
                # make the appropriate changes.

                if { ([info exists tls_servername] ) } {
                    # Look for a matching servername in the Data Group.

                    set ssl_profile [class match -value [string tolower $tls_servername] equals "/Common/tls_servername"]
                    if { $ssl_profile == "" } {
                        # No match, so We will disable the HTTP profile and
                        # set a flag to disable the server side SSL profile.

                        HTTP::disable
                        set sslDisable 1
                    }
                    else {
                        # A match was found in the Data Group, so we will change the SSL
                        # profile to the one we found. Hide this activity from the iRules
                        # parser.

                        set ssl_profile_enable "SSL::profile $ssl_profile"
                        catch { eval $ssl_profile_enable }
                        SSL::enable
                        set sslDisable 0
                    }
                }
                else {
                    # No match because no SNI field was present. We will disable the HTTP
                    # profile and set a flag to disable the server side SSL profile.

                    HTTP::disable
                    set sslDisable 1
                }
            }
            else {
                # We're not in a handshake. Keep on using the currently set SSL profile
                # for this transaction.

                SSL::enable
                set sslDisable 0
            }
            # Hold down any further processing and release the TCP session further
            # down the event loop.

            set detect_handshake 0
            TCP::release
        }
        else {
            # We've not been able to match an SNI field to an SSL profile. We will
            # disable the HTTP profile and set a flag to disable the server side SSL profile

            set detect_handshake 0
            HTTP::disable
            set sslDisable 1
            TCP::release

        }
    }
}

when SERVER_CONNECTED {
    # Check to see if the tls_servername is present

    if { ([info exists tls_servername] ) } {
        # If the tls_servername is present we will change the server SSL profile appropriately.

        SSL::profile "${tls_servername}_serverSSL"
    }

    # Check to see if the server side sslDisable flag is set.

    if {$sslDisable} {
        # If the flag is set we will disable server side SSL profiles.

        SSL::disable
    }
}
3
Rate this Discussion
Comments on this Discussion
Comment made 13-Feb-2015 by Stephan Manthey 3803
Hi Brad, nice and usefull. As well in other scenarios. I was hoping the TLS extensions are following ASN.1 structures. But a quick search wasn´t successful. In this case you could use the recently added related functions to avoid the binary scan voodoo. I would add the "--" into the class commands to terminate option procession. Just in case $tls_servername begins with a hyphen. At least I stumbled across this a while ago. Thanks and +1, Stephan
0
Comment made 13-Feb-2015 by Brad Parker 4475
Good call on the double hyphen. What functions are you referring to rather than the binary scans?
0
Comment made 14-Jun-2015 by boneyard 5621
can i assume it isn't easily possible to grab the SNI value with an state as SSL::SNI or such?
0
Comment made 09-Jul-2015 by Brad Parker 4475
You can grab the SNI value from SSL::extensions, but that requires an SSL event to use and once you've entered an SSL even such as CLIENTSSL_CLIENTHELLO its already too late to disable the SSL profiles used for the proxy/offload.
0

Replies to this Discussion