on 22-Dec-2015 12:59
Recently I was speaking with a customer and they mentioned that they leveraged the “Max Sessions Per User” setting within BIG-IP APM Access Profile to limit the number of concurrent connections that a single user could have at any point in time. The customer mentioned that this works but often their users would complain that the “wrong” session was terminated or that a session they were actively using was closed. After reproducing the scenario in a lab environment I observed that the BIG-IP APM would terminate sessions based on FIFO (First In, First Out). Meaning that the oldest session was always terminated first regardless of which session the user was actively interacting with. Since this was confusing for the customer I figured others experienced this problem and it would be worth sharing my solution with the world. So how do you enforce “Max Sessions Per User” and enable your users to intelligently select which session to terminate? |
If we break down the problem statement above we can see that it it is really two issues. First we need to identify if a user has exceeded the maximum number of allocated sessions, then if they have we need to provide them a way to select which session should be terminated.
BIG-IP APM Access Profiles natively provide a way to limit the Maximum number of Sessions a user can establish but it doesn’t provide a way to interact with pre-existing sessions.
For more information on Access Profile settings see => https://support.f5.com/kb/en-us/products/big-ip_apm/manuals/product/apm-network-access-11-6-0/9.html...
If the built-in functionality won’t achieve what we want lets build our own using APM iRule Events.
The ACCESS::uuid iRule function will allow us to identify all active sessions for a specified Access Profile and Username.
The iRule below will prevent a user from establishing more than 3 sessions
when CLIENT_ACCEPTED { ACCESS::restrict_irule_events disable } when ACCESS_POLICY_COMPLETED { set max_sessions 3 set apm_username [ACCESS::session data get session.logon.last.username] set apm_cookie_list [ ACCESS::uuid getsid "[PROFILE::access name].$apm_username" ] log local0. "[PROFILE::access name].$apm_username => session number [llength $apm_cookie_list]" if {[llength $apm_cookie_list] >= $max_sessions} { ACCESS::session remove ACCESS::respond 302 location "/vdesk/hangup.php3" } }
This will allow us to limit concurrent connections for a user but is too late in the authentication process to enable the user to select a session to terminate.
Instead of having the logic execute in the ACCESS_POLICY_COMPLETED section let’s try using an VPE iRule event.
First update your Access Policy to look similar to the images below with an iRule Event placed after the user authentication event.
Update the iRule you created earlier with the code below, this will allow the VPE policy to pause and execute events within the iRule.
when ACCESS_POLICY_AGENT_EVENT { switch [ACCESS::policy agent_id] { "max_session_count" { set apm_username[ACCESS::session data get session.logon.last.username] set apm_cookie_list [ ACCESS::uuid getsid "[PROFILE::access name].$apm_username" ] ACCESS::session data set session.logon.last.count[llength $apm_cookie_list] } } }
Now as long as the user is below the defined maximum allowed connections they will be allowed to connect as normal.
Now this is the tricky part, we need to provide an interface to the user that will enable them to select a session to be terminated. We could spend a bunch of time creating a custom web interface using javascript or we could re-purpose the Logon Page object built into the APM and display the information to the user with minimal customization.
Next update the iRule created earlier with the snippet listed below. The updated iRule will populate the session variables that will be used to display session information to the user.
when ACCESS_POLICY_AGENT_EVENT { switch [ACCESS::policy agent_id] { "max_session_count" { set apm_username[ACCESS::session data get session.logon.last.username] set apm_cookie_list[ ACCESS::uuid getsid "[PROFILE::access name].$apm_username" ] for {set i 0} {$i < [llength $apm_cookie_list]} {incr i} { set _clientip[ACCESS::session data get -sid [lindex $apm_cookie_list $i] session.user.clientip] set _starttime[ACCESS::session data get -sid [lindex $apm_cookie_list $i] session.user.starttime] set _timeformat[clock format $_starttime -format "%H:%M:%S %d %b %Y "] set _connectiontype[ACCESS::session data get -sid [lindex $apm_cookie_list $i]session.user.sessiontype] set _browsertype[ACCESS::session data get -sid [lindex $apm_cookie_list $i]session.client.type] set _sessionid[ACCESS::session data get -sid [lindex $apm_cookie_list $i] session.user.sessionid] set _sessioninfo"<table style='border-collapse: collapse' width='100%'>" append _sessioninfo"<tr><td style='border: 1px solid black'>Session ID</td><td style='border: 1px solid black' align='center'>$_sessionid</td></tr>" append _sessioninfo"<tr><td style='border: 1px solid black'>Start Time</td><td style='border: 1px solid black' align='center'>$_timeformat</td></tr>" append _sessioninfo"<tr><td style='border: 1px solid black'>Client IP</td><td style='border: 1px solid black' align='center'>$_clientip</td></tr>" append _sessioninfo"<tr><td style='border: 1px solid black'>Connection Type</td><td style='border: 1px solid black' align='center'>$_connectiontype</td></tr>" append _sessioninfo"<tr><td style='border: 1px solid black'>Browser Type</td><td style='border: 1px solid black' align='center'>$_browsertype</td></tr>" append _sessioninfo"</table>" ACCESS::session data set session.logon.active.$i.sid $_sessionid ACCESS::session data set session.logon.active.$i.text $_sessioninfo } ACCESS::session data set session.logon.last.count[llength $apm_cookie_list] } } }
Now that we have a way to select a session to terminate add a second VPE iRule Event to handle the Session Termination
Next update the iRule created earlier with the snippet listed below. The updates will add a second iRule Event that will handle the session termination
when ACCESS_POLICY_AGENT_EVENT { switch [ACCESS::policy agent_id] { "max_session_count" { set apm_username[ACCESS::session data get session.logon.last.username] set apm_cookie_list[ ACCESS::uuid getsid "[PROFILE::access name].$apm_username" ] for {set i 0} {$i < [llength $apm_cookie_list]} {incr i} { set _clientip[ACCESS::session data get -sid [lindex $apm_cookie_list $i] session.user.clientip] set _starttime[ACCESS::session data get -sid [lindex $apm_cookie_list $i] session.user.starttime] set _timeformat[clock format $_starttime -format "%H:%M:%S %d %b %Y "] set _connectiontype[ACCESS::session data get -sid [lindex $apm_cookie_list $i]session.user.sessiontype] set _browsertype[ACCESS::session data get -sid [lindex $apm_cookie_list $i]session.client.type] set _sessionid[ACCESS::session data get -sid [lindex $apm_cookie_list $i] session.user.sessionid] set _sessioninfo"<table style='border-collapse: collapse' width='100%'>" append _sessioninfo"<tr><td style='border: 1px solid black'>Session ID</td><td style='border: 1px solid black' align='center'>$_sessionid</td></tr>" append _sessioninfo"<tr><td style='border: 1px solid black'>Start Time</td><td style='border: 1px solid black' align='center'>$_timeformat</td></tr>" append _sessioninfo"<tr><td style='border: 1px solid black'>Client IP</td><td style='border: 1px solid black' align='center'>$_clientip</td></tr>" append _sessioninfo"<tr><td style='border: 1px solid black'>Connection Type</td><td style='border: 1px solid black' align='center'>$_connectiontype</td></tr>" append _sessioninfo"<tr><td style='border: 1px solid black'>Browser Type</td><td style='border: 1px solid black' align='center'>$_browsertype</td></tr>" append _sessioninfo"</table>" ACCESS::session data set session.logon.active.$i.sid $_sessionid ACCESS::session data set session.logon.active.$i.text $_sessioninfo } ACCESS::session data set session.logon.last.count[llength $apm_cookie_list] } "terminate_session" { set removed0 set apm_username[ACCESS::session data get session.logon.last.username] set apm_cookie_list [ ACCESS::uuid getsid "[PROFILE::access name].$apm_username" ] for {set i 0} {$i < [llength $apm_cookie_list]} {incr i} { set _terminateid[ACCESS::session data get session.logon.last.terminate] set _sessionid[ACCESS::session data get -sid [lindex $apm_cookie_list $i] session.user.sessionid] if {$_sessionid eq $_terminateid} { set removed 1 ACCESS::session remove -sid [lindex $apm_cookie_list $i] break } } ACCESS::session data set session.logon.last.terminateresult$removed } } }
Now establish enough sessions to exceed the maximum concurrent user count and you should see receive a logon page prompting you to select a session to terminate.
When this step is complete your Access Policy should look similar to the attached imaged
Remove the Password field from the Logon Page object and replace it with Radio and set the variable name to terminate
when ACCESS_POLICY_AGENT_EVENT { switch [ACCESS::policy agent_id] { "max_session_count" { set apm_username[ACCESS::session data get session.logon.last.username] set apm_cookie_list[ ACCESS::uuid getsid "[PROFILE::access name].$apm_username" ] for {set i 0} {$i < [llength $apm_cookie_list]} {incr i} { set _clientip[ACCESS::session data get -sid [lindex $apm_cookie_list $i] session.user.clientip] set _starttime[ACCESS::session data get -sid [lindex $apm_cookie_list $i] session.user.starttime] set _timeformat[clock format $_starttime -format "%H:%M:%S %d %b %Y "] set _connectiontype[ACCESS::session data get -sid [lindex $apm_cookie_list $i]session.user.sessiontype] set _browsertype[ACCESS::session data get -sid [lindex $apm_cookie_list $i]session.client.type] set _sessionid[ACCESS::session data get -sid [lindex $apm_cookie_list $i] session.user.sessionid] set _sessioninfo"<table style='border-collapse: collapse' width='100%'>" append _sessioninfo"<tr><td style='border: 1px solid black'>Session ID</td><td style='border: 1px solid black' align='center'>$_sessionid</td></tr>" append _sessioninfo"<tr><td style='border: 1px solid black'>Start Time</td><td style='border: 1px solid black' align='center'>$_timeformat</td></tr>" append _sessioninfo"<tr><td style='border: 1px solid black'>Client IP</td><td style='border: 1px solid black' align='center'>$_clientip</td></tr>" append _sessioninfo"<tr><td style='border: 1px solid black'>Connection Type</td><td style='border: 1px solid black' align='center'>$_connectiontype</td></tr>" append _sessioninfo"<tr><td style='border: 1px solid black'>Browser Type</td><td style='border: 1px solid black' align='center'>$_browsertype</td></tr>" append _sessioninfo"</table>" ACCESS::session data set session.logon.active.$i.sid $_sessionid ACCESS::session data set session.logon.active.$i.text $_sessioninfo } ACCESS::session data set session.logon.last.count[llength $apm_cookie_list] } "terminate_session" { set removed0 set apm_username[ACCESS::session data get session.logon.last.username] set apm_cookie_list [ ACCESS::uuid getsid "[PROFILE::access name].$apm_username" ] for {set i 0} {$i < [llength $apm_cookie_list]} {incr i} { set _terminateid[ACCESS::session data get session.logon.last.terminate] set _sessionid[ACCESS::session data get -sid [lindex $apm_cookie_list $i] session.user.sessionid] if {$_sessionid eq $_terminateid} { set removed 1 ACCESS::session remove -sid [lindex $apm_cookie_list $i] break } } ACCESS::session data set session.logon.last.terminateresult$removed } } }
Is there a way to restrict APM session per user per URI in HTTP_REQUEST? I want to restrict APM to run very first time it find URI /abc/session.svc since that service being called couple of times in communication but only first URI call has username and password. Currently it fails because when it runs seconds time it doesn't get password in URI /abc/session.svc. following iRULE:
when HTTP_REQUEST {
switch [HTTP::method]
{
"COPY" -
"MOVE"
{
Replace Destination header with http if using SSL Offloading
if { [HTTP::header Destination] starts_with "https" }
{
HTTP::header replace Destination [string map -nocase {https http} [HTTP::header value Destination]]
}
HTTP::disable
}
"MKCOL" -
"PROPPATCH"
{
HTTP::disable
}
}
if { ( [HTTP::uri] contains "/abc/session.svc")}
{
ACCESS::enable
HTTP::header insert "clientless-mode" 1
set apmsessionid [HTTP::cookie value MRHSession]
if { [HTTP::cookie exists "MRHSession"]} {set apmstatus [ACCESS::session exists -state_allow $apmsessionid]} else {set apmstatus 0}
if {!($apmstatus)}
{
Insert Clientless-mode header to start APM in clientless mode
if { [catch {HTTP::header insert "clientless-mode" 1} ] } {log local0. "[IP::client_addr]:[TCP::client_port] : TCL error on HTTP header insert clientless-mode : URL : [HTTP::host][HTTP::path] - Headers : [HTTP::request]"}
}
if {([HTTP::method] eq "POST") }
{
set clength 0
if {[HTTP::header exists "Content-Length"] && [HTTP::header Content-Length] <= 1048576}
{
set clength [HTTP::header Content-Length]
} else { set clength 1048576 }
if { [info exists clength] && $clength > 0} { HTTP::collect $clength }
}
} else { ACCESS::disable}
}
when HTTP_REQUEST_DATA {
Parse XML Data
set xmluserdata [findstr [HTTP::payload] "" 10 ""]
set xmluser [findstr $xmluserdata ">" 1 ""]
set xmlpw [findstr $xmluserdata "" 10 end]
}
when ACCESS_SESSION_STARTED {
Variables from HTTP REQUEST Data (XML Parsing)
if {([info exists "xmluser"])} { ACCESS::session data set session.logon.last.username $xmluser; ACCESS::session data set session.logon.last.userid $xmluserdata; }
if {([info exists "xmlpw"])} { ACCESS::session data set session.logon.last.password $xmlpw; }
if {([info exists "xmluserdata"])} { ACCESS::session data set session.logon.last.useridpass $xmlpw; }
}
when ACCESS_ACL_ALLOWED {
set user [ACCESS::session data get session.logon.last.username]
HTTP::header insert "login" $user
HTTP::header remove "Authorization"
}
Found a bug:
set apm_username [ACCESS::session data get session.logon.last.username]
set apm_cookie_list [ ACCESS::uuid getsid "[PROFILE::access name].$apm_username" ]
If the user logs in with IAmAUser and iamauser then that is two different logins based on the above.