Last updated on: November 17 2023.

iRule for Multi-app deploymentΒΆ

###############################################################################
# Bportal iRule v1.0
#
# Change history:
#   2023-02-10: v1.0 release
#
# Note 1:
#   Please consider setting this iRule before any other iRule. Uncommenting the line below is an option to achieve that.
#   priority 100
#
# Note 2:
#   Consider commenting out the call to bp-dumpDGtoLogs in RULE_INIT if this behavior is not desirable.
#
###############################################################################

###### 
# Logging

proc bp-log { severity message } {
	switch -- $severity {
		"debug" { ACCESS::log accesscontrol.debug "$message" }
		"info" { ACCESS::log accesscontrol.info "$message"}
		"warning" {ACCESS::log accesscontrol.warn "$message"}
		"error" { ACCESS::log accesscontrol.error "$message"}
		default { ACCESS::log accesscontrol.info "$message"}
	}
}



proc bp-dumpDGtoLogs { dgname } {
	set content [class get ${dgname}]
	log local0. "Content of datagroup ${dgname}:"
	foreach i ${content} {
		set proto ""
		set backend_fqdn ""
		set pool_name ""
		set ssl_profile ""
		set acl_name ""
		set sso_name ""
		set logout_uri ""
		foreach { proto backend_fqdn pool_name ssl_profile acl_name sso_name logout_uri } [split [lindex $i 1] ","] break
		log local0. "    [lindex $i 0] : Protocol:\[${proto}\], Backend:\[${backend_fqdn}\], Pool:\[${pool_name}\], ClientSSL:\[${ssl_profile}\], ACL:\[${acl_name}\], SSO:\[${sso_name}\], Logout:\[${logout_uri}\]"
	}
}

###### 
# Change Host and Referer headers

proc bp-headersTranslate { proto backend } {
	# Change Host
	if { [HTTP::header exists Host] } {
		call bp-log debug "Rewriting Host from [HTTP::header Host] to $backend"
		HTTP::header replace Host ${backend}
		call bp-log debug "New Host: [HTTP::header Host]"
	}
	# Change Referer
	if { [HTTP::header exists Referer] } {
		call bp-log debug "Rewriting Referer from [HTTP::header Referer] to ${proto}://${backend}"
		HTTP::header replace Referer [regsub {https?://[^/]+/} [HTTP::header Referer] "${proto}://${backend}/"]
		call bp-log debug "New Referer: [HTTP::header Referer]"
	}
	call bp-log debug "Removing LastMRH_Session cookie"
	HTTP::cookie remove LastMRH_Session
}

###### 
# Set session variale session.logout to current time

proc bp-setLogoutTime {} {
	if { [ACCESS::session data get {session.logout}] eq "" } {
		ACCESS::session data set {session.logout} [clock clicks -milliseconds]
	}
}

######
# Check if the session needs to be logged out
# Return 1 if it needs a logout

proc bp-needLogout {} {
	set sess-logout [ACCESS::session data get {session.logout}]
	if { ${sess-logout} eq "" } { 
		# Session is not marked for logout
		return 0
	}
	if { [expr { [clock clicks -milliseconds] - ${static::LOGOUT_DELAY} > ${sess-logout} }] } {
		# Session is marked for logout and LOGOUT_DELAY is over
		return 1
	}
	return 0
}

###### 
# SNI processing
# Source: https://community.f5.com/t5/technical-articles/ssl-orchestrator-use-case-inbound-sni-switching/ta-p/285590

proc bp-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
}

##############################################################################

when RULE_INIT {
	set static::DATAGROUP "sites2publish-dg"
	set static::LOGOUT_DELAY 10000

	# Writes content of the datagroup containing sites definitions to /var/log/ltm
	after 1000 {
		call bp-dumpDGtoLogs ${static::DATAGROUP}
	}
}

when CLIENT_ACCEPTED {
	TCP::collect
	ACCESS::restrict_irule_events disable
}

#####
# Change Client SSL profile based on SNI

when CLIENT_DATA {
	# Obtain SNI
	set sni [call bp-getSNI [TCP::payload]]
	call bp-log debug "SNI: $sni"

	# Lookup SNI in data group
	set site [class lookup ${sni} ${static::DATAGROUP}]
	call bp-log debug "Site: $site"

	# Set variables based on site data (http|https,backend_fqdn,pool_name,ssl_profile,acl_name,sso_name,logout_uri)
	# 7 variables: proto, backend_fqdn, pool_name, ssl_profile, acl_name, sso_name, logout_uri
	# Note: Expectation is that variables referring to configuration objects contain the full path to the object (ie: /Common/myobject)
	#
	set proto ""
	set backend_fqdn ""
	set pool_name ""
	set ssl_profile ""
	set acl_name ""
	set sso_name ""
	set logout_uri ""
	foreach { proto backend_fqdn pool_name ssl_profile acl_name sso_name logout_uri } [split ${site} ","] break 
	call bp-log debug "CLIENT_DATA: Site variables: $proto, $backend_fqdn, $pool_name, $ssl_profile, $acl_name, $sso_name, $logout_uri"

	## Change Client SSL profile based on SNI
	if { ${ssl_profile} ne "" } {
		call bp-log debug "CLIENT_DATA: Current connection: Client IP/port -> VS IP/port : [IP::remote_addr]/[TCP::client_port] -> [IP::local_addr]/[TCP::local_port]"
		call bp-log debug "CLIENT_DATA: Setting client-ssl profile $ssl_profile..."
		set cmd "SSL::profile ${ssl_profile}" ; eval $cmd
		unset cmd
	}
	TCP::release
}

when HTTP_REQUEST {
	call bp-log debug "HTTP_REQUEST: [HTTP::method] [HTTP::host][HTTP::uri] HTTP/[HTTP::version] ([IP::remote_addr]:[TCP::client_port] -> [IP::local_addr]:[TCP::local_port])"

	if { [HTTP::uri] eq "/favicon.ico" } {
		call bp-log debug "HTTP_REQUEST: redirecting favicon.ico to transient-favicon.ico -  ([IP::remote_addr]:[TCP::client_port] -> [IP::local_addr]:[TCP::local_port])"
		HTTP::redirect /transient-favicon.ico
		return
	}
	if { [HTTP::uri] eq "/transient-favicon.ico" } {
		if { ![ACCESS::session exists -state_allow] } {
			# Bypass ACCESS filter if the session is invalid (older session or no session at all). 
			# If session is valid, hostname translation is going to happen in ACCESS_ACL_ALLOWED event.
			call bp-log debug "HTTP_REQUEST: Allowing favicon request without enforcing Access Policy (/transient-favicon.ico) -  ([IP::remote_addr]:[TCP::client_port] -> [IP::local_addr]:[TCP::local_port])"
			call bp-headersTranslate ${proto} ${backend_fqdn}
			ACCESS::disable
		}
	}
}

when ACCESS_ACL_ALLOWED {
	call bp-log debug "ACL_ALLOWED: [HTTP::method] [HTTP::host][HTTP::uri] HTTP/[HTTP::version] ([IP::remote_addr]:[TCP::client_port] -> [IP::local_addr]:[TCP::local_port])"
	call bp-log debug "ACL_ALLOWED: Site variables: $proto, $backend_fqdn, $pool_name, $ssl_profile, $acl_name, $sso_name, $logout_uri"

	# Check if the session needs to be logged out 
	if { [call bp-needLogout] } {
		call bp-log info "Logout has been detected for this session and timer has expired. Removing session now."
		ACCESS::respond 302 noserver Location /vdesk/hangup.php3 Connection close
		ACCESS::session remove
		return
	}


	#Translate Host/Referer headers, removes APM internal cookie
	call bp-log debug "ACL_ALLOWED: Translate headers from ${sni} to ${backend_fqdn}"
	call bp-headersTranslate ${proto} ${backend_fqdn}

	# Assign pool
	if { ${pool_name} ne "" } {
		call bp-log debug "ACL_ALLOWED: Assign pool ${pool_name}"
		pool ${pool_name}
	}

	# Assign ACL
	if { ${acl_name} ne "" } {
		call bp-log debug "ACL_ALLOWED: Eval ACL $acl_name"
		ACCESS::acl eval ${acl_name} 
	}

	# Assign SSO
	if { ${sso_name} ne "" } {
		call bp-log debug "ACL_ALLOWED: Select SSO ${sso_name}"
		WEBSSO::select ${sso_name}
	}

	# Detect logout
	if { ${logout_uri} ne "" } {
		if { [HTTP::uri] matches_glob ${logout_uri} } {
			call bp-log debug "ACL_ALLOWED: Logout detected (pattern / uri: ${logout_uri} / [HTTP::uri])"
			call bp-setLogoutTime
		}
	}

}

when HTTP_REQUEST_SEND {
	call bp-log debug "HTTP_REQUEST_SEND: [HTTP::method] [HTTP::host][HTTP::uri] HTTP/[HTTP::version] ([IP::client_addr]:[TCP::client_port] -> [IP::local_addr clientside]:[TCP::local_port clientside] -BIGIP- [IP::local_addr]:[TCP::local_port] -> [IP::server_addr]:[TCP::server_port])"

	# Translate back the favicon URI from transient-favicon.ico to favicon.ico before sending to backend server.
	if { [HTTP::path] eq "/transient-favicon.ico" } {
		HTTP::uri "/favicon.ico"
		call bp-log debug "HTTP_REQUEST_SENT: Received request for /transient-favicon.ico. Rewriting to /favicon.ico and sending request."
	}
}

when HTTP_REQUEST_RELEASE {
	call bp-log debug "HTTP_REQUEST_RELEASE: [HTTP::method] [HTTP::host][HTTP::uri] HTTP/[HTTP::version] ([IP::client_addr]:[TCP::client_port] -> [IP::local_addr clientside]:[TCP::local_port clientside] -BIGIP- [IP::local_addr]:[TCP::local_port] -> [IP::server_addr]:[TCP::server_port])"
}

when SERVER_CONNECTED {
	# Disable server ssl profile if needed (proto: http)
	call bp-log debug "SERVER_CONNECTED: [IP::client_addr]:[TCP::client_port] -> [IP::local_addr clientside]:[TCP::local_port clientside] -BIGIP- [IP::local_addr]:[TCP::local_port] -> [IP::server_addr]:[TCP::server_port]"
	if { ${proto} eq "http" } {
		call bp-log debug "SERVER_CONNECTED: Disabling Server-ssl profile for HTTP back-end"
		SSL::disable
	} else {
		call bp-log deug "SERVER_CONNECTED: Keep Server-ssl profile for HTTPS back-end"
	}
}

when HTTP_RESPONSE {
	call bp-log debug "HTTP_RESPONSE: [HTTP::status] (Self IP:port <- Server IP:port : [IP::local_addr]:[TCP::client_port] <- [IP::remote_addr]:[TCP::remote_port])"
}

when HTTP_RESPONSE_RELEASE {
	call bp-log debug "HTTP_RESPONSE_RELEASE: [HTTP::status] (Client IP:port <- VS IP:port : [IP::remote_addr]:[TCP::client_port] <- [IP::local_addr]:[TCP::local_port])"
}



# vim: set expandtab ts=4 sw=4 softtabstop=4 syntax=tcl: