Forum Discussion

Jason_Witt_4207's avatar
Jason_Witt_4207
Historic F5 Account
Mar 10, 2006

Fun with ldap

For anyone that is interested, I recently was posed with the problem of proxying ldap requests. The requirement was to send read and write requests to different pools. As any familiar with ldap knows, you need to send a bind request to authenticate. The following will transparently resend the bind requests to the newly selected server prior to sending the new read/write request.

Other functionality could be added to this, such as more verification of the ldap fields to ensure a valid request is being made.


class ldap_writes {
   6
   8
   10
   12
}
rule ldap_proxy {
when RULE_INIT {
     Read Pool
    set ::readPool sun_ldap_read
     Write Pool
    set ::writePool sun_ldap_write
     Turn on debugging
    set ::debug 0
     A lookup table for debugging
    array set ::msg_types {
           0 "bind request"
           1 "bind response"
           2 "unbind request"
           3 "search request"
           4 "search response"
           6 "modify request"
           7 "modify response"
           8 "add request"
           9 "add response"
          10 "delete request"
          11 "delete response"
          12 "modifydn request"
          13 "modifydn response"
          14 "compare request"
          15 "compare response"
          16 "abandon request"
          23 "extended request"
          24 "extended response"
    }
}
when CLIENT_ACCEPTED {  
    set rebind 0
    set binding ""
    set replayop ""
    set writing 0
    TCP::collect
}
 
when CLIENT_DATA {
     Grab the current payload collected
    set payload [TCP::payload]
     Pull the first 2 bytes.  
    binary scan $payload H2c ber_t ber_len
     The first byte is the tag signifying an LDAP message, 
     Always is hex 30, if that is not so reject
    if { $ber_t ne "30" } {
        reject
        return
    }
     The second byte is one of two values:
           a) The length of the packet minus the above
              defining byte and the length byte
        OR 
           b) an octet describing how many subsequent bytes
              hold the packet length
     In either case the message type (what we are after)
     follows the message id field which too can be a variable
     number of bytes. 
    set len_bytes 0
    if { [expr [expr ($ber_len + 0x100) % 0x100] & 128] > 0 } {
        set len_bytes [expr [expr ($ber_len + 0x100) % 0x100] & 127]
    }
     How many bytes is the message id
    binary scan $payload x[expr 3 + $len_bytes]c msgid_bytes
     The message type is then 4 bytes + number length bytes + number of
     message id bytes offset.
    binary scan $payload x[expr 4 + $len_bytes + $msgid_bytes]c msgtype
     msgtype - BER encoded value, bits 1-5 are the actual 
     type, 6 is the data type, 7-8 are the data class  
     Here we only care about the lower 5 bits
    set msgtype [expr $msgtype & 31]
    if {$::debug and 
        [catch {
              log local0. "message type is: $::msg_types($msgtype) $msgtype"
         }
        ] 
       } {
       log local. "Bad message type: $msgtype"
       reject
    }
     Each connection should start with a bind request
     We'll save this packet for later rebinding when we 
     flip between servers
    if { $msgtype == 0 } {
       if {$::debug} {log local0. "Bind Request with: ldap_read"}
       set writing 0
       set rebind 0
       set binding $payload
       LB::detach
       pool $::readPool
     If we come across a write request and are currently not 
     sending data to the write pool, detach, and set the rebind 
     flag so we can send the bind packet before we actually send 
     our write request
    } elseif {[matchclass $msgtype equals $::ldap_writes] and $writing != 1} { 
       if {$::debug} {log local0. "Rebinding with: ldap_write"}
       set rebind 1
       set writing 1
       set replayop $payload
       TCP::payload replace 0 [TCP::payload length] $binding
       LB::detach
       pool $::writePool
     If we come across a read request while we are bound to a write server
     we need to detach and rebind with a read server from the read pool
    } elseif {![matchclass $msgtype equals $::ldap_writes] and $writing == 1} {
       if {$::debug} {log local0. "Rebinding with: ldap_read"}
       set rebind 1
       set writing 0
       set replayop $payload
       TCP::payload replace 0 [TCP::payload length] $binding
       LB::detach
       pool $::readPool
    }
    TCP::release
    TCP::collect
}
when SERVER_CONNECTED {
     A change in the type of request has been detected
     requiring a rebind, we've sent the bind now we need to
     wait for the response before we send the actual request
    if { $rebind == 1 } {
        TCP::collect
    }
}
when SERVER_DATA {
    if { $rebind == 1 } {
        set rebind 0
         See above for details on this block.  Stupid iRules, no proc grrrr
        set payload [TCP::payload]
    
         Pull the first 2 bytes.  
        binary scan $payload H2c ber_t ber_len
        set len_bytes 0
        if { [expr [expr ($ber_len + 0x100) % 0x100] & 128] > 0 } {
            set len_bytes [expr [expr ($ber_len + 0x100) % 0x100] & 127]
        }
            
        binary scan $payload x[expr 3 + $len_bytes]c msgid_bytes
        binary scan $payload x[expr 4 + $len_bytes + $msgid_bytes]c msgtype
        set msgtype [expr $msgtype & 31]
         If the msgtype we have here is for a bind response just discard 
         it as we don't need to send it to the client
        if {$msgtype == 1 } {
            TCP::payload replace 0 [TCP::payload length] ""
        }
         Now send the actual read or write op to the server
         It should now have processed the bind
        TCP::respond $replayop
    }
    TCP::release
}
}

18 Replies

  • we have to use SSL:: instead of TCP:: command since it is SSL.

    this is my configuration. anyway, i never tested ldap write.

    root@ve1110(Active)(/Common)(tmos) list ltm virtual bar
    ltm virtual bar {
        destination 172.28.19.252:636
        ip-protocol tcp
        mask 255.255.255.255
        profiles {
            myclientssl {
                context clientside
            }
            serverssl-insecure-compatible {
                context serverside
            }
            tcp { }
        }
        rules {
            myrule
        }
        snat automap
        vlans-disabled
    }
    root@ve1110(Active)(/Common)(tmos) list ltm profile client-ssl myclientssl
    ltm profile client-ssl myclientssl {
        app-service none
        cert salmon.crt
        defaults-from clientssl
        key salmon.key
    }
    root@ve1110(Active)(/Common)(tmos) list ltm data-group internal ldap_writes
    ltm data-group internal ldap_writes {
        records {
            6 { }
            8 { }
            10 { }
            12 { }
        }
        type integer
    }
    root@ve1110(Active)(/Common)(tmos) list ltm pool sun_ldap_read
    ltm pool sun_ldap_read {
        members {
            200.200.200.103:636 {
                address 200.200.200.103
            }
        }
    }
    root@ve1110(Active)(/Common)(tmos) list ltm pool sun_ldap_write
    ltm pool sun_ldap_write {
        members {
            200.200.200.113:636 {
                address 200.200.200.113
            }
        }
    }
    root@ve1110(Active)(/Common)(tmos) list ltm rule myrule
    ltm rule myrule {
        when RULE_INIT {
         Read Pool
        set static::readPool sun_ldap_read
    
         Write Pool
        set static::writePool sun_ldap_write
    
         Turn on debugging
        set static::ldap_debug 1
    
         A lookup table for debugging
        array set static::msg_types {
               0 "bind request"
               1 "bind response"
               2 "unbind request"
               3 "search request"
               4 "search response"
               6 "modify request"
               7 "modify response"
               8 "add request"
               9 "add response"
              10 "delete request"
              11 "delete response"
              12 "modifydn request"
              13 "modifydn response"
              14 "compare request"
              15 "compare response"
              16 "abandon request"
              23 "extended request"
              24 "extended response"
        }
    }
    
    when CLIENTSSL_HANDSHAKE {
        set rebind 0
        set binding ""
        set replayop ""
        set writing 0
        SSL::collect
    }
    
    when CLIENTSSL_DATA {
         Grab the current payload collected
        set payload [SSL::payload]
    
         Pull the first 2 bytes.
        binary scan $payload H2c ber_t ber_len
    
         The first byte is the tag signifying an LDAP message,
         Always is hex 30, if that is not so reject
    
        if { $ber_t ne "30" } {
            reject
            return
        }
    
         The second byte is one of two values:
               a) The length of the packet minus the above
                  defining byte and the length byte
            OR
               b) an octet describing how many subsequent bytes
                  hold the packet length
         In either case the message type (what we are after)
         follows the message id field which too can be a variable
         number of bytes.
    
        set len_bytes 0
        if { [expr [expr ($ber_len + 0x100) % 0x100] & 128] > 0 } {
            set len_bytes [expr [expr ($ber_len + 0x100) % 0x100] & 127]
        }
    
         How many bytes is the message id
        binary scan $payload x[expr 3 + $len_bytes]c msgid_bytes
    
         The message type is then 4 bytes + number length bytes + number of
         message id bytes offset.
        binary scan $payload x[expr 4 + $len_bytes + $msgid_bytes]c msgtype
    
         msgtype - BER encoded value, bits 1-5 are the actual
         type, 6 is the data type, 7-8 are the data class
         Here we only care about the lower 5 bits
        set msgtype [expr $msgtype & 31]
    
        if {$static::ldap_debug and
            [catch {
                  log local0. "message type is: $static::msg_types($msgtype) $msgtype"
             }
            ]
           } {
           log local0. "Bad message type: $msgtype"
           reject
        }
    
         Each connection should start with a bind request
         We'll save this packet for later rebinding when we
         flip between servers
        if { $msgtype == 0 } {
           if {$static::ldap_debug} {log local0. "Bind Request with: ldap_read"}
           set writing 0
           set rebind 0
           set binding $payload
           LB::detach
           pool $static::readPool
    
         If we come across a write request and are currently not
         sending data to the write pool, detach, and set the rebind
         flag so we can send the bind packet before we actually send
         our write request
        } elseif {[matchclass $msgtype equals ldap_writes] and $writing != 1} {
           if {$static::ldap_debug} {log local0. "Rebinding with: ldap_write"}
           set rebind 1
           set writing 1
           set replayop $payload
           SSL::payload replace 0 [SSL::payload length] $binding
           LB::detach
           pool $static::writePool
    
         If we come across a read request while we are bound to a write server
         we need to detach and rebind with a read server from the read pool
        } elseif {![matchclass $msgtype equals ldap_writes] and $writing == 1} {
           if {$static::ldap_debug} {log local0. "Rebinding with: ldap_read"}
           set rebind 1
           set writing 0
           set replayop $payload
           SSL::payload replace 0 [SSL::payload length] $binding
           LB::detach
           pool $static::readPool
        }
        SSL::release
        SSL::collect
    }
    
    when SERVER_CONNECTED {
         A change in the type of request has been detected
         requiring a rebind, we've sent the bind now we need to
         wait for the response before we send the actual request
        if { $rebind == 1 } {
            SSL::collect
        }
    }
    
    when SERVERSSL_DATA {
        if { $rebind == 1 } {
            set rebind 0
             See above for details on this block.  Stupid iRules, no proc grrrr
            set payload [TCP::payload]
    
             Pull the first 2 bytes.
            binary scan $payload H2c ber_t ber_len
            set len_bytes 0
            if { [expr [expr ($ber_len + 0x100) % 0x100] & 128] > 0 } {
                set len_bytes [expr [expr ($ber_len + 0x100) % 0x100] & 127]
            }
    
            binary scan $payload x[expr 3 + $len_bytes]c msgid_bytes
    
            binary scan $payload x[expr 4 + $len_bytes + $msgid_bytes]c msgtype
    
            set msgtype [expr $msgtype & 31]
    
             If the msgtype we have here is for a bind response just discard
             it as we don't need to send it to the client
            if {$msgtype == 1 } {
                SSL::payload replace 0 [SSL::payload length] ""
            }
             Now send the actual read or write op to the server
             It should now have processed the bind
            SSL::respond $replayop
        }
        SSL::release
    }
    }
    
    [root@ve1110:Active] config  tail -f /var/log/ltm
    Apr 19 02:44:31 tmm info tmm[7321]: Rule /Common/myrule : message type is: bind request 0
    Apr 19 02:44:31 tmm info tmm[7321]: Rule /Common/myrule : Bind Request with: ldap_read
    Apr 19 02:44:31 tmm info tmm[7321]: Rule /Common/myrule : message type is: search request 3
    Apr 19 02:44:31 tmm info tmm[7321]: Rule /Common/myrule : message type is: unbind request 2
    
  • Thanks heaps, nitass.

     

     

    It seems to be working fine for both LDAP read and write over ssl in my test environment.

     

     

    One question though, is it a typo in your irule? TCP::payload or SSL::payload?

     

     

    when SERVERSSL_DATA {

     

    if { $rebind == 1 } {

     

    set rebind 0

     

    See above for details on this block. Stupid iRules, no proc grrrr

     

    set payload [TCP::payload]

     

  • One question though, is it a typo in your irule? TCP::payload or SSL::payload? oops, thanks. it should be SSL::payload.
  • Hamish's avatar
    Hamish
    Icon for Cirrocumulus rankCirrocumulus
    If you're using v10 or above, it might be better to replace all the matchclass'es with their [class .. ] equivalent. The class command is more flexible, and has better performance that the deprecated functions.

     

     

    The class command is documented here -> https://devcentral.f5.com/wiki/iRules.class.ashx

     

     

    H
  • I have updated from

     

     

    } elseif {[matchclass $msgtype equals ldap_writes] and $writing != 1} {

     

    to

     

    } elseif {[class match $msgtype equals ldap_writes] and $writing != 1} {

     

     

    Is it correct?

     

  • Hi All

     

     

    Looking for Irule that will change an LDAPSEARCH as below

     

     

    Current Query:

     

     

    ldapsearch -x -H -D cn=ldapmon,o=services -W -l 121 -z 3 "(&(uid= E13859)(objectclass=Person))" 1.1

     

     

    new Query

     

     

    ldapsearch -x -H -D cn=ldapmon,o=services -W -b cn=E13859,o=users 1.1

     

     

    Search for the value after uid=”” and put that in the new query after cn=””