はじめに:
今回は、一般的によく使われているApache webserverの機能をBIG-IPに持たせるためのiRuleを紹介します。この機能により、複数のURLやポート番号などを使うWebサイトを、クライアントには1つのURL、ポート番号として見せることができます。

タイトル:
Apacheの機能をBIG-IPに持たせるiRule

メリット:
このiRuleにより、普段はサーバで行う処理をBIG-IPで行うことが可能になり、以下のようなメリットが生じます。
・サーバ側で変更があった場合、全てのサーバを変更せずにBIG-IPで対応できるため、管理の手間とコストを削減できる
・ブラウザでURLが1つに見えるので、ユーザにわかりやすい
・別のポートを使うサイトにリダイレクトされた場合、Firewallでブロックされることを避けられる

機能解説:
今回紹介するiRuleはクライアント側とサーバ側のHostname、ポート番号を書き換えることでApacheのmod_proxyモジュール”ProxyPass”及び”ProxyPassReverse”機能をBIG-IPに持たせます。

たとえば以下のようなサイトがあります:
www.company.com
しかし、サポートのアクセスはこちら:
www.support.com/clarify/
そして、ショッピングサイトは:
store.company.com:8080/

Proxypass機能を使うことで、異なるdomainやポートでなく、同じdomainとポート番号を使うことが出来ますが、サーバ側では従来通り別々のDomainとポートを使います。



設定概要:

このiRuleは、以下の4 Stepで簡単にセットアップできます。

1.バーチャルサーバを定義します(HTTP/HTTPS)
2.HTTPとOneConnectプロファイルをバーチャルサーバに適用します
3.ProxyPass iRuleをバーチャルサーバに設定します
4.iRuleで使うData Groupを作成します。このData Groupに書き換えるURL等を定義します。Data Groupの名前はProxyPass[バーチャルサーバ名]です。(たとえばバーチャルサーバ名がXYZの場合は、Data Group名はProxyPassXYZとなります。
また、Data Groupの中の定義は以下のようになります:
[www.external.com www.internal.com:8080]
この例では、www.external.comへのリクエストはBIG-IPで書き換えられ、ポート8080宛てにwww.internal.comへ送信されます。

なお、今月のiRuleの使い方やメリットについては下記のURLでもご確認いただけます:
http://devcentral.f5.com/Wiki/default.aspx/iRules/proxypass.html

iRule定義:


when RULE_INIT {
# Enable to debug ProxyPass translations via log messages in /var/log/ltm
# (2 = verbose, 1 = essential, 0 = none)
# TMOSv10: You can optionally change this global variable to a static
# to make this iRule CMP-friendly (change references within iRule too).
# set static::ProxyPassDebug 0
set ::ProxyPassDebug 0

# Enable to rewrite page content (try a setting of 1 first)
# (2 = attempt to rewrite host/path and just /path, 1 = attempt to rewrite host/path)
# TMOSv10: You can optionally change this global variable to a static
# to make this iRule CMP-friendly (change references within iRule too).
# set static::RewriteResponsePayload 0
set ::RewriteResponsePayload 0
}

when CLIENT_ACCEPTED {
# Get the default pool name. This is used later to explicitly select
# the default pool for requests which don't have a pool specified in
# the class.
set default_pool [LB::server pool]

if { $::ProxyPassDebug > 1 } {
log local0. "[virtual name]: [IP::client_addr]:[TCP::client_port] -> [IP::local_addr]:[TCP::local_port]"
}
}

when HTTP_REQUEST {
# "bypass" tracks whether or not we made any changes inbound so we
# can skip changes on the outbound traffic for greater efficiency.
set bypass 1

# The name of the Data Group (aka class) we are going to use
# TMOSv10: use this line for TMOSv10 (and comment out the line below it)
#set clname "ProxyPass[virtual name]"
set clname "::ProxyPass[virtual name]"

# Initialize other local variables used in this rule
set orig_uri "[HTTP::uri]"
set orig_host "[HTTP::host]"
set log_prefix "[virtual name], Host=$orig_host, URI=$orig_uri"
set clientside ""
set serverside ""
set newpool ""
set snataddr ""
set ppass ""

# TMOSv10: use this line for TMOSv10 (and comment out the line below it)
# (don't forget to move the curly brace to the other line)
#if {! [class exists $clname]}
if {! [info exists $clname]} {
# Data Group not defined: do not do anything further and exit this iRule
log local0. "$log_prefix: Data Group $clname not found."
pool $default_pool
return
}

set match_len 0
if { $::ProxyPassDebug > 1 } {
log local0. "$log_prefix: Looking for entries matching $orig_host$orig_uri"
}
# TMOSv10: use this line for TMOSv10 (and comment out the line below it)
# (don't forget to move the curly brace to the other line)
#foreach entry [class names $clname]
foreach entry [set [set clname]] {
# Look through data group to find an entry where the host/uri
# starts with the clientside portion of an entry. Track the
# length of the match to find the longest match (more specific).
if {"$orig_host$orig_uri" starts_with [getfield $entry " " 1]} {
set new_len [string length [getfield $entry " " 1]]
if {$new_len > $match_len} {
set ppass $entry
set match_len $new_len
}
}
}
if {$ppass eq ""} {
# We did not find an entry with the hostname, now try just the URI.
# Look through data group to find an entry where the uri
# starts with the clientside portion of an entry. Track the
# length of the match to find the longest match (more specific).

if { $::ProxyPassDebug > 1 } {
log local0. "$log_prefix: Looking for entries matching $orig_uri"
}
# TMOSv10: use this line for TMOSv10 (and comment out the line below it)
# (don't forget to move the curly brace to the other line)
#foreach entry [class names $clname]
foreach entry [set [set clname]] {
if {$orig_uri starts_with [getfield $entry " " 1]} {
set new_len [string length [getfield $entry " " 1]]
if {$new_len > $match_len} {
set ppass $entry
set match_len $new_len
}
}
}
}

if {$ppass eq ""} {
# No entries found, stop processing this request
if { $::ProxyPassDebug > 1 } {
log local0. "$log_prefix: No rule found"
}
pool $default_pool
return
}

# Store each entry in the data group line into a local variable
set clientside [getfield $ppass " " 1]
set serverside [getfield $ppass " " 2]
set newpool [getfield $ppass " " 3]
set snataddr [getfield $ppass " " 4]

if {[substr $clientside 0 1] eq "/"} {
# No virtual hostname specified, so use the Host header instead
set host_clientside $orig_host
set path_clientside $clientside
} else {
# Virtual host specified in entry, split the host and path
set host_clientside [getfield $clientside "/" 1]
set path_clientside [substr $clientside [string length $host_clientside]]
}
# At this point $host_clientside is the client hostname, and $path_clientside
# is the client-side path as specified in the data group

set host_serverside [getfield $serverside "/" 1]
set path_serverside [substr $serverside [string length $host_serverside]]
if {$host_serverside eq ""} {
set host_serverside $host_clientside
}
# At this point $host_serverside is the server hostname, and $path_serverside
# is the server-side path as specified in the data group

# In order for directory redirects to work properly we have to be careful with slashes
if {$path_clientside equals "/"} {
# Make sure serverside path ends with / if clientside path is "/"
if {!($path_serverside ends_with "/")} {
append path_serverside "/"
}
} else {
# Otherwise, neither can end in a / (unless serverside path is just "/")
if {$path_serverside ends_with "/"} {
if {!($path_serverside equals "/")} {
set path_serverside [string trimright $path_serverside /]
}
}
if {$path_clientside ends_with "/"} {
set path_clientside [string trimright $path_clientside /]
}
}

if { $::ProxyPassDebug > 0 } {
log local0. "$log_prefix: Found Rule, Client Host=$host_clientside, Client Path=$path_clientside, Server Host=$host_serverside, Server Path=$path_serverside"
}

# As you may or may not know, if you go to http://www.domain.com/dir, and /dir is a directory, the web
# server will redirect you to http://www.domain.com/dir/. The problem is, with ProxyPass, if the client-side
# path is http://www.domain.com/dir, but the server-side path is http://www.domain.com/, the server will NOT
# redirect the client (it isn't going to redirect you to http://www.domain.com//!). Here is the problem with
# that. If there is an image referenced on the page, say logo.jpg, the client doesn't realize /dir is a directory
# and as such it will try to load http://www.domain.com/logo.jpg and not http://www.domain.com/dir/logo.jpg. So
# ProxyPass has to handle the redirect in this case. This only really matters if the server-side path is "/",
# but since we have the code here we might as well offload all of the redirects that we can (that is whenever
# the client path is exactly the client path specified in the data group but not "/").
if {$orig_uri eq $path_clientside} {
if {[string index $path_clientside end] ne "/"} {
set is_https 0
if {[PROFILE::exists clientssl] == 1} {
set is_https 1
}
# Assumption here is that the browser is hitting http://host/path which is a virtual path and we need to do the redirect for them
if {$is_https == 1} {
HTTP::redirect "https://$orig_host$orig_uri/"
if { $::ProxyPassDebug } {
log local0. "$log_prefix: Redirecting to https://$orig_host$orig_uri/"
}
} else {
HTTP::redirect "http://$orig_host$orig_uri/"
if { $::ProxyPassDebug } {
log local0. "$log_prefix: Redirecting to http://$orig_host$orig_uri/"
}
}
return
}
}

# The following code does the actual rewrite on its way TO
# the backend server. It replaces the URI with the newly
# constructed one and masks the "Host" header with the FQDN
# the backend pool server wants to see.
#
# If a new pool or custom SNAT are to be applied, these are
# done here as well. If a SNAT is used, an X-Forwarded-For
# header is attached to send the original requesting IP
# through to the server.

if {$host_clientside eq $orig_host} {
if {$orig_uri starts_with $path_clientside} {
# Do not bypass the iRule in the response
set bypass 0
if { $::ProxyPassDebug > 1 } {
log local0. "$log_prefix: New Host=$host_serverside, New Path=$path_serverside[substr $orig_uri [string length $path_clientside]]"
}
# Rewrite the URI
HTTP::uri $path_serverside[substr $orig_uri [string length $path_clientside]]
# Rewrite the Host header
HTTP::header replace Host: $host_serverside
# Now alter the Referer header if necessary
if { [HTTP::header exists "Referer"] } {
set protocol [substr [HTTP::header "Referer"] 0 $host_clientside]
if {[string length $protocol] > 0} {
set client_path [findstr [HTTP::header "Referer"] $host_clientside [string length $host_clientside]]
if {$client_path starts_with $path_clientside} {
if { $::ProxyPassDebug > 1 } {
log local0. "$log_prefix: Changing Referer header: [HTTP::header Referer] with $protocol$host_serverside$path_serverside[substr $client_path [string length $path_clientside]]"
}
HTTP::header replace "Referer" $protocol$host_serverside$path_serverside[substr $client_path [string length $path_clientside]]
}
}
}
# Take care of pool selection and SNAT settings
if {$newpool eq ""} {
pool $default_pool
if { $::ProxyPassDebug > 1 } {
log local0. "$log_prefix: Using default pool $default_pool"
}
} else {
pool $newpool
if { $::ProxyPassDebug > 0 } {
log local0. "$log_prefix: Using parsed pool $newpool"
}
}
if {$snataddr != ""} {
snat $snataddr
if { $::ProxyPassDebug > 0 } {
log local0. "$log_prefix: Using SNAT address $snataddr"
}
HTTP::header insert "X-Forwarded-For" "[IP::remote_addr]"
}
}
}

# If we're rewriting the response content, prevent the server from using
# compression in its response by removing the Accept-Encoding header
# from the request. LTM does not decompress response content before
# applying the stream profile. This header is only removed if we're
# rewriting response content.
if { $::RewriteResponsePayload } {
if { [HTTP::header exists "Accept-Encoding"] } {
HTTP::header remove "Accept-Encoding"
if { $::ProxyPassDebug > 1} {
log local0. "$log_prefix: Removed Accept-Encoding header"
}
}
}
}

when HTTP_RESPONSE {
if {$bypass} {
# No modification is necessary if we didn't change anything inbound

# Check if we're rewriting the response
if {$::RewriteResponsePayload} {
if { $::ProxyPassDebug > 1 } {
log local0. "$log_prefix: Rewriting response content enabled, but disabled on this response."
}

# Need to explicity disable the stream filter if it's not needed for this response
# Hide the command from the iRule parser so it won't generate a validation error
# when not using a stream profile
set stream_disable_cmd "STREAM::disable"

# Execute the STREAM::disable command. Use catch to handle any errors. Save the result to $result
if { [catch {eval $stream_disable_cmd} result] } {
# There was an error trying to disable the stream profile.
log local0. "$log_prefix: Error disabling stream filter ($result). If you enable \$::RewriteResponsePayload, then you should add a stream profile to the VIP. Else, set \$::RewriteResponsePayload to 0 in this iRule."
}
}

# Exit from this event.
return
}

# Check if we're rewriting the response
if {$::RewriteResponsePayload} {
# Configure and enable the stream filter to rewrite the response payload
# Hide the command from the iRule parser so it won't generate a validation error
# when not using a stream profile
if {$::RewriteResponsePayload > 1} {
set stream_expression_cmd "STREAM::expression \"@$host_serverside$path_serverside@$host_clientside$path_clientside@ @$path_serverside@$path_clientside@\""
} else {
set stream_expression_cmd "STREAM::expression \"@$host_serverside$path_serverside@$host_clientside$path_clientside@\""
}
set stream_enable_cmd "STREAM::enable"
if { $::ProxyPassDebug > 1 } {
log local0. "$log_prefix: \$stream_expression_cmd: $stream_expression_cmd, \$stream_enable_cmd: $stream_enable_cmd"
}

# Execute the STREAM::expression command. Use catch to handle any errors. Save the result to $result
if { [catch {eval $stream_expression_cmd} result] } {
# There was an error trying to set the stream expression.
log local0. "$log_prefix: Error setting stream expression ($result). If you enable \$::RewriteResponsePayload, then you should add a stream profile to the VIP. Else, set \$::RewriteResponsePayload to 0 in this iRule."
} else {
# No error setting the stream expression, so try to enable the stream filter
# Execute the STREAM::enable command. Use catch to handle any errors. Save the result to $result
if { [catch {eval $stream_enable_cmd} result] } {
# There was an error trying to enable the stream filter.
log local0. "$log_prefix: error enabling stream filter ($result): $result"
} else {
if { $::ProxyPassDebug > 1 } {
log local0. "$log_prefix: Successfully configured and enabled stream filter"
}
}
}
}

# Fix Location, Content-Location, and URI headers
foreach header {"Location" "Content-Location" "URI"} {
set protocol [substr [HTTP::header $header] 0 $host_serverside]
if {$protocol ne ""} {
set server_path [findstr [HTTP::header $header] $host_serverside [string length $host_serverside]]
if {$server_path starts_with $path_serverside} {
if { $::ProxyPassDebug } {
log local0. "$log_prefix: Changing response header $header: [HTTP::header $header] with $protocol$host_clientside$path_clientside[substr $server_path [string length $path_serverside]]"
}
HTTP::header replace $header: $protocol$host_clientside$path_clientside[substr $server_path [string length $path_serverside]]
}
}
}

# Rewrite any domains/paths in Set-Cookie headers
if {[HTTP::header exists "Set-Cookie"]}{
array set cookielist { }
# A response may have multiple Set-Cookie headers, loop through them
foreach cookievalue [HTTP::header values "Set-Cookie"] {
set cookiename [getfield $cookievalue "=" 1]
set newcookievalue ""
# Each cookie starts with name=value and then has more name/value pairs
foreach element [split $cookievalue ";"] {
set element [string trim $element]
if {$element contains "="} {
set elementname [getfield $element "=" 1]
set elementvalue [getfield $element "=" 2]
if {$elementname eq "domain"} {
# Rewrite domain of cookie, if necessary.
if {$elementvalue eq $host_serverside} {
if {$::ProxyPassDebug > 1} {
log local0. "Modifying cookie $cookiename domain from $elementvalue to $host_clientside"
}
set elementvalue $host_clientside
}
}
if {$elementname eq "path"} {
# Rewrite path of cookie, if necessary.
if {$elementvalue starts_with $path_serverside} {
if {$::ProxyPassDebug > 1} {
log local0. "Modifying cookie $cookiename path from $elementvalue to $path_clientside[substr $elementvalue [string length $path_serverside]]"
}
set elementvalue $path_clientside[substr $elementvalue [string length $path_serverside]]
}
}
append newcookievalue "$elementname=$elementvalue; "
} else {
append newcookievalue "$element; "
}
}
# Store new cookie value for later re-insertion. The cookie value
# string will end with an extra "; " so strip that off here.
set cookielist($cookiename) [string range $newcookievalue 0 [expr {[string length $newcookievalue] - 3}]]
}
# Remove all Set-Cookie headers and re-add them (modified or not)
HTTP::header remove "Set-Cookie"
foreach cookiename [array names cookielist] {
HTTP::header insert "Set-Cookie" $cookielist($cookiename)
if {$::ProxyPassDebug > 1} {
log local0. "Inserting cookie: $cookielist($cookiename)"
}
}
}
}

# Only uncomment this event if you need extra debugging for content rewriting.
# This event can only be uncommented if the iRule is used with a stream profile.
#when STREAM_MATCHED {
# if { $::ProxyPassDebug } {
# log local0. "$log_prefix: Rewriting match: [STREAM::match]"
# }
#}

# The following code will look up SSL profile rules from
# the Data Group List "ProxyPassSSLProfiles" and apply
# them.
#
# The format of the entries in this list is as follows:
#
#
#
# All entries are separated by spaces, and both items
# are required. Failure to set them will result in an
# error message.
when SERVER_CONNECTED {
if {$bypass} {
return
}

if {! [info exists ::ProxyPassSSLProfiles]} {
return
}

set pool [LB::server pool]
set profilename [findclass $pool ProxyPassSSLProfiles " "]

if {$profilename eq ""} {
if { [PROFILE::exists serverssl] == 1} {
# Hide this command from the iRule parser (in case no serverssl profile is applied)
set disable "SSL::disable serverside"
catch {eval $disable}
}
return
}

if { $::ProxyPassDebug > 0 } {
log local0. "$log_prefix: ServerSSL profile $profilename assigned for pool $pool"
}
if { [PROFILE::exists serverssl] == 1} {
# Hide these commands from the iRule parser (in case no serverssl profile is applied)
set profile "SSL::profile $profilename"
catch {eval $profile}
set enable "SSL::enable serverside"
catch {eval $enable}
} else {
log local0. "$log_prefix: ServerSSL profile must be defined on virtual server to enable server-side encryption!"
}
}

※F5ネットワークスジャパンでは、サンプルコードについて検証を実施していますが、お客様の使用環境における動作を保証するものではありません。実際の使用にあたっては、必ず事前にテストを実施することを推奨します。