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.0
No CommentsBe the first to comment