Citrix XenApp Secure Access Deployment
Problem this snippet solves:
With the combination of BIG-IP Access Policy Manager (APM) and Citrix ""XenApp"", organizations can deliver a complete remote access solution that allows for scalability, security, compliance and flexibility. The following iRule provides the functionality for a secure proxy connection from various Citrix clients (PN Agent, Dazzle, Receiver and Web Browser) without the need for additional clients installed on the devices.
How to use this snippet:
Deployment Guide: https://f5.com/solutions/deployment-guides/citrix-xenapp-or-xendesktop-release-candidate-big
Code :
rule APM_Citrix {
when RULE_INIT {
set tmm_apm_pnagent_url "/Citrix/PNAgent/config.xml"
}
when CLIENT_ACCEPTED {
TCP::collect 7
}
when CLIENT_DATA {
# Disable SSL if it's HTTP CONNECT request
if { [TCP::payload 7] equals "CONNECT" } {
SSL::disable
}
TCP::release
}
when HTTP_REQUEST {
set tmm_apm_host [HTTP::host]
set tmm_apm_uri_path [HTTP::path]
set tmm_apm_user_agent [HTTP::header "User-Agent"]
set tmm_apm_http_method [HTTP::method]
set tmm_apm_session_id ""
set tmm_apm_citrix_receiver 0
set tmm_apm_citrix_pnagent 0
set tmm_apm_citrix_ica_patching 0
set tmm_apm_vip "$tmm_apm_host:[TCP::local_port clientside]"
log -noname accesscontrol.local1.debug "01490000:3: Request [HTTP::request]"
if { [HTTP::cookie exists "MRHSession"] } {
set tmm_apm_session_id [HTTP::cookie "MRHSession"]
}
if { $tmm_apm_user_agent contains "CitrixReceiver" } {
set tmm_apm_citrix_receiver 1
} elseif { $tmm_apm_user_agent contains "PNAMAIN" or $tmm_apm_user_agent contains "Dazzle" } {
set tmm_apm_citrix_pnagent 1
}
if { $tmm_apm_http_method equals "CONNECT" } {
# Handle the secure proxy connect requests. Return a Proxy-Authenticate header
# field with a challenge if the user is not authenticated.
if { ![HTTP::header exists "Proxy-Authorization"] } {
HTTP::respond 407 Proxy-Authenticate "Basic realm=\"123\""
return
}
set authstr [lindex [ split [HTTP::header "Proxy-Authorization"] " " ] 1 ]
# Seems like the Citrix base64 encoding logic has a bug that terminates
# the input string with a null byte when the extra padding characters are
# added. We remove the extra null character before we decode it.
set remainder [lindex [split [expr [string length $authstr] / 4.0 ] "." ] 1]
if { $remainder != "0" } {
if { [regsub -all {(A=)} $authstr = newstring] > 0 } {
set authstr $newstring
}
}
#Decoded string format: 52553eb5b18572cdbe7dda4a8220bf35:172.30.6.197-1494
set apm_session [ lindex [ split [b64decode $authstr] ":" ] 0 ]
if { ![ACCESS::session exists $apm_session] } {
HTTP::respond 407 Proxy-Authenticate "Basic realm=\"123\""
return
}
# User is authenticated, send the traffic to the connect proxy virtual.
log -noname accesscontrol.local1.notice "01490000:3: Request for citrix resource received from session: $apm_session"
ACCESS::disable
use virtual citrix_connect_proxy
}
if { ($tmm_apm_session_id == "") && ($tmm_apm_citrix_pnagent == 1) } {
if { $tmm_apm_uri_path equals $::tmm_apm_pnagent_url } {
ACCESS::disable
return
}
# If the client is PNAgent or Dazzle, extract the credentials from the
# payload and insert them in HTTP headers.
HTTP::header insert "clientless-mode" 1
HTTP::header insert "username" ""
HTTP::header insert "password" ""
if { ![info exists tmm_apm_citrix_username] && [HTTP::header exists Content-Length] } {
HTTP::collect [HTTP::header Content-Length]
}
}
if { $tmm_apm_citrix_receiver == 1 } {
# Collect the user credentials and set ready for access policy validation
if { $tmm_apm_uri_path equals "/cgi/login" } {
HTTP::header insert "clientless-mode" 1
HTTP::header insert "username" ""
HTTP::header insert "password" ""
HTTP::cookie remove MRHSession
HTTP::collect [HTTP::header Content-Length]
} elseif { $tmm_apm_uri_path equals "/ipad" } {
set AD_only "citrixreceiver://createprofile/?s=$tmm_apm_host&pname=Profile-$tmm_apm_host&gw=1&gwt=2&gwa=1"
set RSA_only "citrixreceiver://createprofile/?s=$tmm_apm_host&pname=Profile-$tmm_apm_host&gw=1&gwt=2&gwa=2"
set AD_RSA "citrixreceiver://createprofile/?s=$tmm_apm_host&pname=Profile-$tmm_apm_host&gw=1&gwt=2&gwa=3"
HTTP::respond 200 content "<html><h2><a href=\"$AD_only\">Click here for domain only auth</a><a href=\"$RSA_only\">Click here for RSA only</a><a href=\"$AD_RSA\">Click here for Two-factor auth</a></h2></html>"
}
}
}
when HTTP_REQUEST_DATA {
if { ($tmm_apm_citrix_pnagent != 1) && ($tmm_apm_citrix_receiver != 1) } {
return
}
set payload [HTTP::payload]
if { $tmm_apm_citrix_receiver == 1 } {
# Parse the user credentials from the payload
log -noname accesscontrol.local1.debug "01490000:3: Parsing credentials for Citrix receiver"
set tmm_apm_citrix_username ""
set tmm_apm_citrix_password ""
set tmm_apm_citrix_password1 ""
set urlvars [ split $payload "&" ]
foreach {u} $urlvars {
set param [ lindex [ split $u "=" ] 0 ]
set value [ lindex [ split $u "=" ] 1 ]
if { $param equals "login" } {
set tmm_apm_citrix_username $value
} elseif { $param equals "passwd" } {
set tmm_apm_citrix_password $value
} elseif { $param equals "passwd1" } {
set tmm_apm_citrix_password1 $value
}
}
# Insert the parsed credentials into the HTTP request as headers
HTTP::header replace "username" $tmm_apm_citrix_username
HTTP::header replace "password" $tmm_apm_citrix_password
HTTP::release
} elseif { $tmm_apm_citrix_pnagent == 1 } {
# Parse the user credentials from the payload
log -noname accesscontrol.local1.debug "01490000:3: Parsing credentials for Citrix PNAgent"
set tmm_apm_citrix_username ""
set tmm_apm_citrix_password ""
if { [regexp -nocase {<username>([^<]+)</username>} $payload dummy tmm_apm_citrix_username] == 0 } {
log -noname accesscontrol.local1.error "01490000:3: $tmm_apm_session_id: Username not found in the PNAgent POST body"
return
}
if { [regexp -nocase {<password[^>]+>([^<]+)</password>} $payload dummy tmm_apm_citrix_password] == 0 } {
log -noname accesscontrol.local1.error "01490000:3: $tmm_apm_session_id: Password not found in the PNAgent POST body"
return
}
# Decode the password
binary scan $tmm_apm_citrix_password c* pass
set len [llength $pass]
set result {}
for { set i 0 } { $i < $len } { incr i } {
set hi [lindex $pass $i]
set hi [ expr { $hi - 0x41 } ]
set hi [ expr { $hi << 4 } ]
incr i
set lo [lindex $pass $i]
set lo [ expr { $lo - 0x41 } ]
set char [ binary format c [expr {$hi + $lo}] ]
append result $char
}
binary scan $result H* pass
binary scan $result c* pass
set len [llength $pass]
set result {}
set first [lindex $pass 0]
set char [ binary format c [expr { $first ^ 0xA5 } ] ]
append result $char
for { set i 1 } { $i < $len } { incr i } {
set prev [ lindex $pass [expr {$i-1}] ]
set curr [ lindex $pass $i ]
set char [ binary format c [ expr {$curr ^ $prev ^ 0xA5} ] ]
append result $char
}
binary scan $result H* pass
set tmm_apm_citrix_password [ regsub -all {\000} $result {} ]
# Insert the parsed credentials into the HTTP request as headers
HTTP::header replace "username" $tmm_apm_citrix_username
HTTP::header replace "password" $tmm_apm_citrix_password
HTTP::release
}
}
when HTTP_RESPONSE {
if { [HTTP::header Content-Type] contains "application/x-ica" } {
set tmm_apm_citrix_ica_patching 1
HTTP::collect [HTTP::header Content-Length]
}
}
when HTTP_RESPONSE_DATA {
# ICA patching:
if { $tmm_apm_citrix_ica_patching == 1 } {
# ICA file patching: Add entries to point citrix clients to the
# Citrix ICA patching virtual as their HTTP proxy. It also sets
# the ProxyUsername to the APM session id to let the Citrix clients
# to connect to the proxy without requesting the user to authenticate
# again.
log -noname accesscontrol.local1.debug "01490000:3: ICA file patching"
set payload [HTTP::payload]
set payload [ regsub -all {Proxy[^\n]+\n} $payload {} ]
set payload [ regsub {DoNotUseDefaultCSL[^\n]+\n} $payload {} ]
if { $tmm_apm_citrix_receiver == 1 } {
set payload [ regsub {CGPAddress[^\n]+\n} $payload {} ]
}
regexp -line {Address=(.+)} $payload dummy CtxAddrPort
set CtxAddr [lindex [split $CtxAddrPort ":"] 0]
set CtxPort [lindex [split $CtxAddrPort ":"] 1]
regexp -line {CGPAddress=(.+)} $payload dummy CGPAddrPort
if { [info exists CGPAddrPort] } {
set CtxPort [lindex [split $CGPAddrPort ":"] 1]
}
set payload [ regsub {\[WFClient\]} $payload "&\r\nProxyType=Secure\r\nProxyHost=$tmm_apm_vip\r\nProxyUsername=$tmm_apm_session_id\r\nProxyPassword=$CtxAddr-$CtxPort" ]
set payload [ regsub {SSLEnable[^\n]+\n} $payload "SSLEnable=On\r\n" ]
set payload [ regsub {Address[^\n]+\n} $payload "Address=$tmm_apm_host\r\n" ]
HTTP::respond 200 content $payload Content-Type [HTTP::header Content-Type]
}
}
when ACCESS_SESSION_STARTED {
if { ($tmm_apm_citrix_receiver == 0) or ![info exists tmm_apm_citrix_password1] } {
return
}
# Pass the domain password as a session variable. Logon page agent doesn't
# take it from HTTP headers in clientless mode.
ACCESS::session data set "session.logon.last.password1" [URI::decode $tmm_apm_citrix_password1]
}
when ACCESS_POLICY_COMPLETED {
if { $tmm_apm_citrix_receiver == 0 } {
return
}
set sid [ACCESS::session data get session.keydb]
set result [ACCESS::policy result]
# Remove the user credential variables
if { [info exists tmm_apm_citrix_username] } {
unset tmm_apm_citrix_username
}
if { [info exists tmm_apm_citrix_password] } {
unset tmm_apm_citrix_password
}
if { [info exists tmm_apm_citrix_password1] } {
unset tmm_apm_citrix_password1
}
# Clear the domain password session variable created at the session validation start.
ACCESS::session data set "session.logon.last.password1" ""
if { $result equals "allow" } {
set resp "<html><head><META HTTP-EQUIV=\"REFRESH\" CONTENT=\"0; URL=$::tmm_apm_pnagent_url\"></head><body></body></html>"
ACCESS::respond 200 content $resp Set-Cookie "MRHSession=$sid;path=/;secure" Set-Cookie "NSC_AAAC=123;path=/;secure"
}
}
}
Updated Jan 26, 2023
Version 2.0Nojan_Moshiri_4
Historic F5 Account
Joined May 07, 2008
Nojan_Moshiri_4
Historic F5 Account
Joined May 07, 2008
No CommentsBe the first to comment