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

Filter by:
  • Solution
  • Technology
Answers

Performing SSL Bypass for Forward Proxy Traffic based using an iRule capturing the SNI

Hey everyone!

I'm currently developing an iRule to exclude certain traffic from the "Full Proxy" Architecture by turning off the HTTP Profile and Client/Server SSL Profile for our SSL Forward Proxy. We are using the built in function in SWG but for some banking applications it still does not seem to work and the SWG's intelligence is getting in the way. We have been hit with a few bugs which we are currently resolving but in the mean time we need to have this iRule in place to create a workaround.

We would like a clean cut for some of the applications they have by adding them to a Data Group and building an iRule for this purpose.

Here is the current iRule:

when RULE_INIT {
  #set 1 to enable logging, 0 to disable
  set static::debug 0
}

when CLIENT_ACCEPTED { 
#This iRule is meant to Passthrough SSL Connections for SWG in order to solve SSL issue. Based on Data Group List of IP addresses.
if { [class match [IP::local_addr] equals DG_SWG_SSL_Passthrough_IP] }
        { 
        SSL::disable clientside
        SSL::disable serverside
        HTTP::disable
        if {$static::debug}{log local0. "ir181017-1 - Client IP: {[IP::client_addr]} Server IP: {[IP::local_addr]}: - Match DataGroup DG_SWG_SSL_Passthrough_IP! Disabling SSL"}
        } 
    }


when CLIENTSSL_CLIENTHELLO { 
#This iRule is meant to Passthrough SSL Connections for SWG in order to solve SSL issues. Based on Data Group List of FQDNs.
if {$static::debug}{log local0. "ir181017-2 - Client IP: {[IP::client_addr]} - Client Request Server SSL SNI: {[SSL::sni name]}"}
if { [class match [string tolower [SSL::sni name]] contains DG_SWG_SSL_Passthrough_FQDN] }
        { 
        SSL::disable clientside
        SSL::disable serverside
        HTTP::disable
        if {$static::debug}{log local0. "ir181017-3 - Client IP: {[IP::client_addr]} - Server SSL SNI: {[SSL::sni name]} - Match DataGroup DG_SWG_SSL_Passthrough_FQDN! Disabling SSL"}
        } 
    }

The most relevant part of the iRule is the CLIENTSSL_CLIENTHELLO section. When logging the entries, we cannot see any SSL::sni in the logs. But when tcpdumping we can clearly see that there is Server Name Indication fields in the traffic.

Perhaps we are using the SSL::sni command wrong. Perhaps we can use the SSL::extension and have it return the SNI from there and we match against that instead?

0
Rate this Question

Answers to this Question

placeholder+image
USER ACCEPTED ANSWER & F5 ACCEPTED ANSWER

Try this code (not tested) and change line 52 with data group name

when CLIENT_ACCEPTED {
    SSL::disable
    SSL::disable serverside               
    TCP::collect
    set tls_servername ""
}
when CLIENT_DATA {
    # Store TCP Payload up to 2^14 + 5 bytes (Handshake length is up to 2^14)
    set payload [TCP::payload 16389]
    set payloadlen [TCP::payload length]

    # If valid TLS 1.X CLIENT_HELLO handshake packet
    if { [binary scan $payload cH4Scx3H4x32c tls_record_content_type tls_version tls_recordlen tls_handshake_action tls_handshake_version tls_handshake_sessidlen] == 6 && \
        ($tls_record_content_type == 22) && ([string match {030[1-3]} $tls_version]) && \
        ($tls_handshake_action == 1) && ($payloadlen == $tls_recordlen+5)} {
        # skip past the session id
        set record_offset [expr {44 + $tls_handshake_sessidlen}]

        # skip past the cipher list
        binary scan $payload @${record_offset}S tls_ciphlen
        set record_offset [expr {$record_offset + 2 + $tls_ciphlen}]

        # skip past the compression list
        binary scan $payload @${record_offset}c tls_complen
        set record_offset [expr {$record_offset + 1 + $tls_complen}]

        # check for the existence of ssl extensions
        if { ($payloadlen > $record_offset) } {
            # skip to the start of the first extension
            binary scan $payload @${record_offset}S tls_extension_length
            set record_offset [expr {$record_offset + 2}]
            # Check if extension length + offset equals payload length
            if {$record_offset + $tls_extension_length == $payloadlen} {
                # for each extension
                while { $record_offset < $payloadlen } {
                    binary scan $payload @${record_offset}SS tls_extension_type tls_extension_record_length
                    if { $tls_extension_type == 0 } {
                        # if it's a servername extension read the servername
                        # SNI record value start after extension type (2 bytes), extension record length (2 bytes), record type (2 bytes), record type (1 byte), record value length (2 bytes) = 9 bytes
                        binary scan $payload @[expr {$record_offset + 9}]A[expr {$tls_extension_record_length - 5}] tls_servername
                        set record_offset [expr {$record_offset + $tls_extension_record_length + 4}]

                    } else {
                        # skip over other extensions
                        set record_offset [expr {$record_offset + $tls_extension_record_length + 4}]
                    }
                }
            }
        }
    } 
    unset -nocomplain payload payloadlen tls_record_content_type tls_recordlen tls_handshake_action tls_handshake_sessidlen record_offset tls_ciphlen tls_complen tls_extension_length tls_extension_type tls_extension_record_length tls_supported_versions_length tls_supported_versions
    if {$tls_servername equals "" || [class match $tls_servername equals "MyDG"] == 0} {
        SSL::enable
        SSL::enable serverside 
    }
    TCP::release
}
0
Comments on this Answer
Comment made 3 months ago by Kevin Stewart

I think the quick answer though, is no. You'll make some possibly incorrect assumptions about the connection if you try to condense the iRule. Bottom line, you have to dig into a "semi-structured" binary TLS ClientHello message to find a blob of text data, and this code has to literally walk through the x509 data to find it.

The cool thing though is that you can shove all of that yucky binary code into a separate reusable proc and call it from your VIP rules:

Separate proc iRule (library-rule):

proc getSNI { payload } {
    set detect_handshake 1

    binary scan ${payload} H* orig
    if { [binary scan [TCP::payload] cSS tls_xacttype tls_version tls_recordlen] < 3 } {
        reject
        return
    }

    # 768 SSLv3.0
    # 769 TLSv1.0
    # 770 TLSv1.1
    # 771 TLSv1.2
    switch $tls_version {
        "769" -
        "770" -
        "771" {
            if { ($tls_xacttype == 22) } {
                binary scan ${payload} @5c tls_action
                if { not (($tls_action == 1) && ([string length ${payload}] > $tls_recordlen)) } {
                    set detect_handshake 0
                }
            }
        }
        "768" {
            set detect_handshake 0
        }
        default {
            set detect_handshake 0
        }
    }

    if { ($detect_handshake) } {
        # skip past the session id
        set record_offset 43
        binary scan ${payload} @${record_offset}c tls_sessidlen
        set record_offset [expr {$record_offset + 1 + $tls_sessidlen}]

        # skip past the cipher list
        binary scan ${payload} @${record_offset}S tls_ciphlen
        set record_offset [expr {$record_offset + 2 + $tls_ciphlen}]

        # skip past the compression list
        binary scan ${payload} @${record_offset}c tls_complen
        set record_offset [expr {$record_offset + 1 + $tls_complen}]

        # check for the existence of ssl extensions
        if { ([string length ${payload}] > $record_offset) } {
            # skip to the start of the first extension
            binary scan ${payload} @${record_offset}S tls_extenlen
            set record_offset [expr {$record_offset + 2}]
            # read all the extensions into a variable
            binary scan ${payload} @${record_offset}a* tls_extensions

            # for each extension
            for { set ext_offset 0 } { $ext_offset < $tls_extenlen } { incr ext_offset 4 } {
                binary scan $tls_extensions @${ext_offset}SS etype elen
                if { ($etype == 0) } {
                    # if it's a servername extension read the servername
                    set grabstart [expr {$ext_offset + 9}]
                    set grabend [expr {$elen - 5}]
                    binary scan $tls_extensions @${grabstart}A${grabend} tls_servername_orig
                    set tls_servername [string tolower ${tls_servername_orig}]
                    set ext_offset [expr {$ext_offset + $elen}]
                    break
                } else {
                    # skip over other extensions
                    set ext_offset [expr {$ext_offset + $elen}]
                }
            }
        }
    }

    if { ![info exists tls_servername] } {
        ## This isn't TLS so we can't decrypt it anyway
        return "null"
    } else {
        return ${tls_servername}
    }
    TCP::release
}

And then in your regular traffic-processing rules attached to the VIPs,

when CLIENT_ACCEPTED {
    TCP::collect
}
when CLIENT_DATA {
    set foo [call library-rule::getSNI [TCP::payload]]
    log local0. "foo = $foo"
    ## do something here with the returned value.
    TCP::release
}
0
Comment made 3 months ago by Stanislas Piron 10236

@Kevin

all the decoding code is working as I described in this link

https://devcentral.f5.com/codeshare/sni-based-pool-selection-without-clientssl-profile-1119

The first 6 fields are required to match TLS protocol. if there is less data, this is not TLS (perhaps SSLv2, but not TLS supporting SNI)

I spent several hours to read TLS v1.2 and 1.3 RFC and write this code and make sure it works in any circonstances with TLS packets. I tested the decode algorithm with tclsh and wireshark captures (the code in the link) with packets versions SSLv2, SSLv3, TLSv1.0, TLSv1.1, TLSv1.2, TLSv1.3 (Draft 28).

It always extracted both SSL/TLS version and Servername informations from packets, except for SSLv2 and SSLv3 which doesn't support SNI ;-)

0
Comment made 3 months ago by Stanislas Piron 10236

@Kevin

I want to correct my comment... because as described in codeshare, I'm not the initial author of the code...

I know Joel and you published codes before, and I worked to understand it reading documentations like RFC... and I tried to improve it and publish.

I only wanted to say that the decoding part of this code was already tested...

0
Comment made 3 months ago by Kevin Stewart

It's all good. I believe Philip was asking if there was a way to extract SNI without all of the TCP binary complexity, so that's the question I was trying to answer. All of these versions work as desired, so it's really just a matter of packaging. Binary manipulation of data is painful in any programming language. ;)

0
Comment made 3 months ago by Philip Jonsson 1018

Thanks for all of the explanations. For someone with still very limited iRule knowledge, the iRule is quite complex for someone like me to read. That is why I thought there might be a way to extract only the SNI. But thanks for that explanation Kevin.

@Stanislas Piron - Thanks for the iRule, I had to make some smaller changes to it in order for it to fully work. But after that it worked flawlessly. Now I just need to include my original IP SSL By-pass irule but that should be quite easy. That way the customer can choose to exclude both IP addresses and FQDN.

I must say, I'm extremely impressed of both you @Kevin and @Stanislas. Your knowledge and contributions to this community are invaluable.

Here is the final iRule that worked for me:

when CLIENT_ACCEPTED {
    HTTP::disable
    SSL::disable clientside
    SSL::disable serverside               
    TCP::collect
    set tls_servername ""
}
when CLIENT_DATA {
    # Store TCP Payload up to 2^14 + 5 bytes (Handshake length is up to 2^14)
    set payload [TCP::payload 16389]
    set payloadlen [TCP::payload length]

    # If valid TLS 1.X CLIENT_HELLO handshake packet
    if { [binary scan $payload cH4Scx3H4x32c tls_record_content_type tls_version tls_recordlen tls_handshake_action tls_handshake_version tls_handshake_sessidlen] == 6 && \
        ($tls_record_content_type == 22) && ([string match {030[1-3]} $tls_version]) && \
        ($tls_handshake_action == 1) && ($payloadlen == $tls_recordlen+5)} {
        # skip past the session id
        set record_offset [expr {44 + $tls_handshake_sessidlen}]

        # skip past the cipher list
        binary scan $payload @${record_offset}S tls_ciphlen
        set record_offset [expr {$record_offset + 2 + $tls_ciphlen}]

        # skip past the compression list
        binary scan $payload @${record_offset}c tls_complen
        set record_offset [expr {$record_offset + 1 + $tls_complen}]

        # check for the existence of ssl extensions
        if { ($payloadlen > $record_offset) } {
            # skip to the start of the first extension
            binary scan $payload @${record_offset}S tls_extension_length
            set record_offset [expr {$record_offset + 2}]
            # Check if extension length + offset equals payload length
            if {$record_offset + $tls_extension_length == $payloadlen} {
                # for each extension
                while { $record_offset < $payloadlen } {
                    binary scan $payload @${record_offset}SS tls_extension_type tls_extension_record_length
                    if { $tls_extension_type == 0 } {
                        # if it's a servername extension read the servername
                        # SNI record value start after extension type (2 bytes), extension record length (2 bytes), record type (2 bytes), record type (1 byte), record value length (2 bytes) = 9 bytes
                        binary scan $payload @[expr {$record_offset + 9}]A[expr {$tls_extension_record_length - 5}] tls_servername
                        set record_offset [expr {$record_offset + $tls_extension_record_length + 4}]

                    } else {
                        # skip over other extensions
                        set record_offset [expr {$record_offset + $tls_extension_record_length + 4}]
                    }
                }
            }
        }
    } 
    unset -nocomplain payload payloadlen tls_record_content_type tls_recordlen tls_handshake_action tls_handshake_sessidlen record_offset tls_ciphlen tls_complen tls_extension_length tls_extension_type tls_extension_record_length tls_supported_versions_length tls_supported_versions
    if {$tls_servername equals "" || [class match $tls_servername contains "TLS_FQDN_Bypass"] == 0} {
        HTTP::enable
        SSL::enable clientside
        SSL::enable serverside 
    }
    TCP::release
}
0
Comment made 3 months ago by Stanislas Piron 10236

I'm glad this helped you.

to manage source IP based exception, change the CLIENT_ACCEPTED event to this :

when CLIENT_ACCEPTED {
    HTTP::disable
    SSL::disable clientside
    SSL::disable serverside
    if { [class match [IP::local_addr] equals DG_SWG_SSL_Passthrough_IP] } {
      # datagroup matches... do not collect DATA
      if {$static::debug}{log local0. "ir181017-1 - Client IP: {[IP::client_addr]} Server IP: {[IP::local_addr]}: - Match DataGroup DG_SWG_SSL_Passthrough_IP! Disabling SSL"}
    } else {
      # Collect TCP DATA for SNI analysis
      TCP::collect
      set tls_servername ""
    }
}
0
Comment made 3 months ago by Philip Jonsson 1018

@Stanislas

I made some modifications to the iRule. Since it by default would turn off the HTTP and SSL profiles, even if the IP addresses would be matched, the statement in the CLIENT_DATA event would turn the functions back on, even if we matched it earlier. So I decided to negate the statement in the CLIENT_DATA and simply go by, if we match the Data Groups, we turn it off.

I have also added a debug statement both before and after the match. That way we can see if the problematic IP addresses/FQDNs is found and if they are not matched we know we will need to add them.

Since I tried this iRule at the customer site, the logs were flooded and hardly readable. So I decided to check the client_addr against a customizable variable. That way, before troubleshooting, we have to specify the client for the troubleshooting session.

I have a few follow-up questions if you don't mind.

  1. Is it a problem that I'm using Global Variables in the RULE_INIT? I checked the CMP status and it is still enabled.
  2. Is there something we can do to optimize the iRule? I mean, if we match against the Data Group list in the CLIENT_ACCEPTED event, then we might as well stop processing the iRule. I noticed you can use the command "return" to accomplish that. Will that have a negative/positive impact on the iRule?

Here is the final iRule:

when RULE_INIT {
  # set 1 to enable logging, 0 to disable
    set static::debug 1
  # Define what source IP you are troubleshooting
    set static::sourceIP 10.10.10.33
}
when CLIENT_ACCEPTED {
if {$static::debug and [IP::client_addr] equals $static::sourceIP}{log local0. "ir181017-1 - Client IP: {[IP::client_addr]} Server IP: {[IP::local_addr]}"}
    if { [class match [IP::local_addr] equals DG_SWG_SSL_Passthrough_Clients_IP] } {
      # If Destination IP address matches Data Group - turn off HTTP and SSL Profiles
        HTTP::disable
        SSL::disable clientside
        SSL::disable serverside
        if {$static::debug and [IP::client_addr] equals $static::sourceIP}{log local0. "ir181017-2 - Client IP: {[IP::client_addr]} Server IP: {[IP::local_addr]}: - Match DataGroup DG_SWG_SSL_Passthrough_Clients_IP! Disabling SSL"}
    } else {
      # Collect TCP DATA for SNI analysis in the CLIENT_DATA event
        TCP::collect
        set tls_servername ""
    }
}
when CLIENT_DATA {
    # Store TCP Payload up to 2^14 + 5 bytes (Handshake length is up to 2^14)
    set payload [TCP::payload 16389]
    set payloadlen [TCP::payload length]

    # If valid TLS 1.X CLIENT_HELLO handshake packet
    if { [binary scan $payload cH4Scx3H4x32c tls_record_content_type tls_version tls_recordlen tls_handshake_action tls_handshake_version tls_handshake_sessidlen] == 6 && \
        ($tls_record_content_type == 22) && ([string match {030[1-3]} $tls_version]) && \
        ($tls_handshake_action == 1) && ($payloadlen == $tls_recordlen+5)} {
        # skip past the session id
        set record_offset [expr {44 + $tls_handshake_sessidlen}]

        # skip past the cipher list
        binary scan $payload @${record_offset}S tls_ciphlen
        set record_offset [expr {$record_offset + 2 + $tls_ciphlen}]

        # skip past the compression list
        binary scan $payload @${record_offset}c tls_complen
        set record_offset [expr {$record_offset + 1 + $tls_complen}]

        # check for the existence of ssl extensions
        if { ($payloadlen > $record_offset) } {
            # skip to the start of the first extension
            binary scan $payload @${record_offset}S tls_extension_length
            set record_offset [expr {$record_offset + 2}]
            # Check if extension length + offset equals payload length
            if {$record_offset + $tls_extension_length == $payloadlen} {
                # for each extension
                while { $record_offset < $payloadlen } {
                    binary scan $payload @${record_offset}SS tls_extension_type tls_extension_record_length
                    if { $tls_extension_type == 0 } {
                        # if it's a servername extension read the servername
                        # SNI record value start after extension type (2 bytes), extension record length (2 bytes), record type (2 bytes), record type (1 byte), record value length (2 bytes) = 9 bytes
                        binary scan $payload @[expr {$record_offset + 9}]A[expr {$tls_extension_record_length - 5}] tls_servername
                        set record_offset [expr {$record_offset + $tls_extension_record_length + 4}]

                    } else {
                        # skip over other extensions
                        set record_offset [expr {$record_offset + $tls_extension_record_length + 4}]
                    }
                }
            }
        }
    } 
    unset -nocomplain payload payloadlen tls_record_content_type tls_recordlen tls_handshake_action tls_handshake_sessidlen record_offset tls_ciphlen tls_complen tls_extension_length tls_extension_type tls_extension_record_length tls_supported_versions_length tls_supported_versions
        if {$static::debug and [IP::client_addr] equals $static::sourceIP}{log local0. "ir181017-3 - Client IP: {[IP::client_addr]} Server IP: {[IP::local_addr]} SNI: {$tls_servername}"}
        if {$tls_servername equals "" || ![class match $tls_servername contains "DG_SWG_SSL_Passthrough_Clients_FQDN"] == 0} {
        HTTP::disable
        SSL::disable clientside
        SSL::disable serverside
        if {$static::debug and [IP::client_addr] equals $static::sourceIP}{log local0. "ir181017-4 - Client IP: {[IP::client_addr]} Server IP: {[IP::local_addr]} SNI: {$tls_servername} - Match DataGroup DG_SWG_SSL_Passthrough_Clients_FQDN! Disabling SSL"}
    }
    TCP::release
}
0
Comment made 3 months ago by Stanislas Piron 10236

Hi,

There is an issue with your code

when CLIENT_ACCEPTED {
if {$static::debug and [IP::client_addr] equals $static::sourceIP}{log local0. "ir181017-1 - Client IP: {[IP::client_addr]} Server IP: {[IP::local_addr]}"}
    if { [class match [IP::local_addr] equals DG_SWG_SSL_Passthrough_Clients_IP] } {
      # If Destination IP address matches Data Group - turn off HTTP and SSL Profiles
        HTTP::disable
        SSL::disable clientside
        SSL::disable serverside
        if {$static::debug and [IP::client_addr] equals $static::sourceIP}{log local0. "ir181017-2 - Client IP: {[IP::client_addr]} Server IP: {[IP::local_addr]}: - Match DataGroup DG_SWG_SSL_Passthrough_Clients_IP! Disabling SSL"}
    } else {
      # Collect TCP DATA for SNI analysis in the CLIENT_DATA event
        TCP::collect
        set tls_servername ""
    }
}

You forgot ssl disable commands in else part!!! This may cause issues with decoding process

You can use static variables, but make sure those are unique on all irules.

For example, if static::debug is configured on multiple irules, the last irule load will change the data and debug will be enabled on all irules...

You are not using global but static variables... global variables start with :: (ex ::debug)

If you don’t collect data, CLIENT_DATA code won’t execute... no need to add return which only exit current event, not the whole irule.

And CLIENT_ACCEPTED event execute only once per TCP connection... CLIENT_DATA event won’t loop on every packets because there is no TCP::collect after the TCP::release command...

0
Comment made 3 months ago by Philip Jonsson 1018

Hey Stanislas

I have reverted back to the original iRule which you posted and trying to get the logging statements into the iRule. This will help the customer of finding more addresses that's not working. But I'm ripping my hair trying to get this to work. The only logging statement is the first one, even though it should trigger the other ones. The iRule itself is working so I'm not sure why it's causing these problems.

Here is my iRule:

when RULE_INIT {
  #set 1 to enable logging, 0 to disable
    set static::tls_iR_debug 1
    set static::tls_iR_sourceIP 10.10.10.33
}
when CLIENT_ACCEPTED {
        HTTP::disable
        SSL::disable clientside
        SSL::disable serverside
            if {$static::tls_iR_debug and [IP::client_addr] equals $static::tls_iR_sourceIP}{log local0. "ir181017-1 - Client IP: {[IP::client_addr]} Server IP: {[IP::local_addr]}"}
            if { [class match [IP::local_addr] equals DG_SWG_SSL_Passthrough_Clients_IP] } {
            # If Destination IP address matches Data Group - turn off HTTP and SSL Profiles  
            } else {
      # Collect TCP DATA for SNI analysis in the CLIENT_DATA event
                if {$static::tls_iR_debug and [IP::client_addr] equals $static::tls_iR_sourceIP}{log local0. "ir181017-2 - Client IP: {[IP::client_addr]} Server IP: {[IP::local_addr]} - No Match!!"}
            TCP::collect
            set tls_servername ""
    }
}
when CLIENT_DATA {
    # Store TCP Payload up to 2^14 + 5 bytes (Handshake length is up to 2^14)
    set payload [TCP::payload 16389]
    set payloadlen [TCP::payload length]
    # If valid TLS 1.X CLIENT_HELLO handshake packet
    if { [binary scan $payload cH4Scx3H4x32c tls_record_content_type tls_version tls_recordlen tls_handshake_action tls_handshake_version tls_handshake_sessidlen] == 6 && \
        ($tls_record_content_type == 22) && ([string match {030[1-3]} $tls_version]) && \
        ($tls_handshake_action == 1) && ($payloadlen == $tls_recordlen+5)} {
        # skip past the session id
        set record_offset [expr {44 + $tls_handshake_sessidlen}]

        # skip past the cipher list
        binary scan $payload @${record_offset}S tls_ciphlen
        set record_offset [expr {$record_offset + 2 + $tls_ciphlen}]

        # skip past the compression list
        binary scan $payload @${record_offset}c tls_complen
        set record_offset [expr {$record_offset + 1 + $tls_complen}]

        # check for the existence of ssl extensions
        if { ($payloadlen > $record_offset) } {
            # skip to the start of the first extension
            binary scan $payload @${record_offset}S tls_extension_length
            set record_offset [expr {$record_offset + 2}]
            # Check if extension length + offset equals payload length
            if {$record_offset + $tls_extension_length == $payloadlen} {
                # for each extension
                while { $record_offset < $payloadlen } {
                    binary scan $payload @${record_offset}SS tls_extension_type tls_extension_record_length
                    if { $tls_extension_type == 0 } {
                        # if it's a servername extension read the servername
                        # SNI record value start after extension type (2 bytes), extension record length (2 bytes), record type (2 bytes), record type (1 byte), record value length (2 bytes) = 9 bytes
                        binary scan $payload @[expr {$record_offset + 9}]A[expr {$tls_extension_record_length - 5}] tls_servername
                        set record_offset [expr {$record_offset + $tls_extension_record_length + 4}]

                    } else {
                        # skip over other extensions
                        set record_offset [expr {$record_offset + $tls_extension_record_length + 4}]
                    }
                }
            }
        }
    }
    unset -nocomplain payload payloadlen tls_record_content_type tls_recordlen tls_handshake_action tls_handshake_sessidlen record_offset tls_ciphlen tls_complen tls_extension_length tls_extension_type tls_extension_record_length tls_supported_versions_length tls_supported_versions
        if {$static::tls_iR_debug and [IP::client_addr] equals $static::tls_iR_sourceIP}{log local0. "ir181017-3 - Client IP: {[IP::client_addr]} Server IP: {[IP::local_addr]} SNI: {$tls_servername}"}
        if {$tls_servername equals "" || [class match $tls_servername contains "DG_SWG_SSL_Passthrough_Clients_FQDN"] == 0} {
        HTTP::enable
        SSL::enable clientside
        SSL::enable serverside
        if {$static::tls_iR_debug and [IP::client_addr] equals $static::tls_iR_sourceIP}{log local0. "ir181017-4 - Client IP: {[IP::client_addr]} Server IP: {[IP::local_addr]} SNI: {$tls_servername} - No Match!!"}
    }
    TCP::release
}
0
Comment made 3 months ago by Philip Jonsson 1018

Hey Stanislas

Never mind the question above. I managed to solve it. It all works as intended now and I'm able to log correctly.

Thanks for all the help and assistance! :)

0
placeholder+image
USER ACCEPTED ANSWER & F5 ACCEPTED ANSWER

Are you working in transparent or explicit mode?

0
Comments on this Answer
Comment made 4 months ago by Philip Jonsson 1018

Hey Stanislas

We're using Transparent Proxy Mode. Sorry, I forgot to mention that. The SWG solution is used for URL filtering on a Guest Wifi so we're not intercepting the SSL traffic.

0
placeholder+image
USER ACCEPTED ANSWER & F5 ACCEPTED ANSWER

Try this (in CLIENTSSL_CLIENTHELLO):

[SSL::extensions exists -type 0]

But also, configuring bypass data groups in the SSL Forward Proxy section of the client SSL profile doesn't work either?

0
Comments on this Answer
Comment made 4 months ago by Philip Jonsson 1018

Hey Kevin

I actually involved an F5 SE here in the Nordics that also mentions that command. He sent me this:

The attached document contain an iRule that are based on iRule from SSL intercept 1.0 iApp (https://downloads.f5.com/esd/ecc.sv?sw=BIG-IP&pro=iApp_Templates&ver=iApps&container=iApp-Templates ) and the iRule show how verify SNI using SSL::extension command in iRule.

when CLIENTSSL_CLIENTHELLO {
    if { [SSL::extensions exists -type 0] } {
       binary scan [SSL::extensions -type 0] @9a* tls_servername
    }
}

But won't the SSL::extension -type 0 just look for the extension and not the actual value? We created an iRule to solve Facebook's TLS 1.3 problem by scanning the payload for a hex value (the cipher suites) and match that against a Data Group List.

But I thought that was a bit too much for such a simple thing as checking the SNI in a ClientHello. The wiki page for SSL::sni is extremely lacking, not defining where it can be used and more importantly how to use it. In the example they use it in a HTTP_REQUEST event and we will never trigger that since our HTTP_REQUEST is encrypted in the SSL session.

Could you perhaps help me put together an example of how the SSL::extension actually matches a value we can compare to the Data Group List?

I will try and get it working as soon as I get access to my lab. I'll go with the extension first but if that does not work I guess i'll have to do a TCP::collect and grab the value from that.

I will also try to use the bypass in the SSL Forward Proxy section of the ClientSSL profile. We have 4 types of SWG solutions running so we'll have to modify each profile. But if it works then we do not have to bother with these iRules.

Thanks so far!

0
Comment made 4 months ago by Kevin Stewart

You're right, the command itself will only return the success|fail result of the search. But the following binary scan does return the SNI value to the tls_servername variable.

when CLIENTSSL_CLIENTHELLO {
    set sni_exists [SSL::extensions exists -type 0]
    if { $sni_exists } {
        binary scan [SSL::extensions -type 0] @9a* tls_servername
    }
    log local0. "SNI = ${tls_servername}"
}

Normally you'd then just need to issue the SSL::forward_proxy policy bypass command in CLIENTSSL_SERVERHELLO_SEND:

when CLIENTSSL_SERVERHELLO_SEND {
    if { [class match ${tls_servername} equals bypass-sites] } {
        SSL::forward_proxy policy bypass
    }
}

This has more or less the same effect as adding the data group to the SSL Forward Proxy section of the client SSL profile.

It's important to understand though that at this point you've already started the client-side SSL processing, so you cannot simply disable the profile as you've done in your iRule. If you really need to disable the client and server SSL profiles, then you have to dig into the binary of the TCP payload. Here's an example of what that would look like:

https://devcentral.f5.com/questions/solution-for-o365-ssl-forward-proxy-bypass-50828

0
Comment made 4 months ago by Philip Jonsson 1018

Ahh I see, I'll try that tomorrow. See how it works.

All right, I figured it'd worked the same. We did the SSL::disable command for Facebook TLS using the following iRule:

when CLIENT_ACCEPTED {
        #log local0. "[virtual]: [IP::remote_addr]:[TCP::remote_port] -> [IP::local_addr]:[TCP::local_port] (Timeout=[IP::idle_timeout])"
        # Create access session to avoid forging certificate for un-authenticated sessions (id 673357)
        if { [ACCESS::session exists] } {
          #log local0. "Found Access Session = [ACCESS::session exists]"
        } else {
          set sid [ACCESS::session create -lifetime 300 -timeout 300 -flow]
          #log local0. "No Access Session found, src [IP::remote_addr] creating $sid"
          ACCESS::session data set session.ui.mode "0"
          ACCESS::session data set session.policy.result "allow"
        }
        # Collect TCP packet for TLS detection in CLIENT_DATA event
        TCP::collect
}
when CLIENT_DATA {
    #Search for TLS 1.3 App Signature (tls 1.3 version number) and disable SSL to avoid handshake failure
    binary scan [TCP::payload] H300 content
    if { [info exists content] } {
      #log local0. "Content: $content"
      if { [class match $content contain DG_TLS_1_3_Hex_List ] } {
        #log local0. "Facebook TLS 1.3 cipher"
        SSL::disable clientside
        SSL::disable serverside
        HTTP::disable
      }
      TCP::release
    }
}

And it worked flawlessly. But in that scenario the SSL handshake has not taken place because it is in the CLIENT_DATA event, right?

Thanks you :)

0
Comment made 4 months ago by Kevin Stewart

Correct. You're still working at layer 4. The problem is that you can't use any of the SSL:: commands until you enable the SSL filter, and by then it's too late to do SSL::disable. So you have to dig into the layer 4 payload to get the SNI.

0
Comment made 4 months ago by Philip Jonsson 1018

Then I guess I have some work to do! But now I have plenty of options. :)

Thanks Kevin!

0
placeholder+image
USER ACCEPTED ANSWER & F5 ACCEPTED ANSWER

To decode tls to extract servername, use this code!

This is an enhanced code based on Joël Moses code

0
Comments on this Answer
Comment made 4 months ago by Stanislas Piron 10236

Or even better, use layered virtual server with ltm policy

https://devcentral.f5.com/articles/sni-routing-with-big-ip-31348?tag=Sni

If sni doesn’t match datagroup, forward to vs with ssl forward proxy

Else do nothing

0
Comment made 3 months ago by Philip Jonsson 1018

Hey Stanislas

I'm thinking the best approach to make sure we're not touching the SSL is to use a binary scan. Creating additional virtual servers could increase complexity and it'd be nice to use the same one.

I have read through the iRules and they are quite long. Is that really necessary in order to withdraw the SNI and simply match against a datagroup list? Or can we make shorter ones. If they are too long the customer will not have a chance of troubleshooting it on their own.

I actually found a shorter one originally created by Kevin Stewart but it still is almost 100 lines.

0
placeholder+image
USER ACCEPTED ANSWER & F5 ACCEPTED ANSWER

.

0