iRule基础知识积累


1. 什么是iRule?

​ 用最简单的术语来说,iRule 是针对通过 F5 设备的网络流量执行的脚本。

​ iRules 使您能够编写简单的、具有网络感知能力的代码段,这些代码段将以各种方式影响您的网络流量。iRules 背后的理念是使 BIG-IP 几乎无限灵活。iRules 是一种网络感知型定制语言,用户可以使用它在网络层部署中添加业务和应用程序逻辑。您可以在下面看到一个基本的 iRule 示例,这就是 iRule 的样子,我们将在本系列的后续部分中更深入地探索 iRule 的不同部分。如果您对代码还不完全满意,请不要让这吓到您,随着系列的继续,我们将深入探讨构建 iRules 所需的每个部分。现在的想法是开始让 iRules 看起来和感觉更熟悉。我们将在后续部分中更深入地探索 iRule 的不同部分。

2. 基础知识

  • command (命令)
  • Variables (变量)
  • Conditionals (条件句)
  • Operators (运算符,分为比较运算符(Comparison Operators)和逻辑运算符(Logical Operators))

3. 变量

​ 变量,简单来说就是存储在内存中的一段数据。这通常是出于在某个时候再次使用该数据的概念,然后在脚本中调用它以使用它。每种脚本语言都有某种形式的变量,如果没有它们,您将构建静态脚本来执行一次性的迭代任务。变量是允许动态编程考虑多个条件和用例的很大一部分。无论是存储来自数学运算的数据以与所需结果进行比较,或者检查是否已经满足给定的条件,变量是编程中几乎所有事物的核心,在现代编码中,我们离不开它。想法很简单,取一段数据,无论是数字还是字符串或……任何东西,并以唯一的名称将其存储在内存中。然后,稍后,您可以通过简单地引用您创建的名称来表示该数据的值。当然,您也可以随意修改或删除变量。

​ 对于任何变量,有两个主要功能:设置和检索。要在 Tcl 中设置变量,您只需使用 set 命令并指定所需的值。这可以是静态或动态值,例如整数或命令的结果。然后将所需值存储在内存中并与提供的变量名称相关联。示例:

1
2
3
#Basic variable creation in Tcl
set integer 5
set hostname [HTTP::host]

要检索与给定变量关联的值,您只需直接引用该变量即可获得结果值。例如,“$integer”和“$hostname”将引用上述示例中的值。

1
2
3
4
#Basic variable reference to retrieve and use the value stored in memory
if {$integer > 0) {
log local0. "Host: $hostname"
}

iRules 中有两种主要类型的变量,局部变量和全局变量。

局部变量

​ 除非另有说明,否则所有变量均作为 iRule 中的局部变量创建。局部变量意味着它被分配了与创建它的 iRule 相同的范围。所有 iRule 本质上都是基于会话的,因此所有局部变量也是基于会话的。这意味着会话规定了给定 iRule 的局部变量和数据的内存空间。例如,如果 connection1 进入并执行 iRule,创建 5 个变量,这些变量将仅存在直到 connection1 关闭并且会话在 BIG-IP 上终止。届时,分配给该流的内存将被释放,并且在处理该特定会话的 iRule 时创建的变量将不再可访问。局部变量成本低,易于使用,您永远不必担心使用它们进行内存管理,因为它们会在会话终止时自动清除。

​ 局部变量将占 iRules 中变量使用的绝大部分。根据具体情况,它们高效、易于使用且非常有用。这些可以直接设置,但也通常是提供输出的命令的结果。还有多种方法可以在 iRules 中引用变量。当使用直接影响变量的命令时,通常用“$”直接引用变量名称,其他时候大括号允许您更清楚地定义变量名称的开头和结尾。一些示例如下所示:

1
2
3
4
5
6
7
8
9
10
11
#Standard variable reference
log local0. "My host is $host"

#Set variable to the output of a particular command using brackets []
set int [expr {5 + 8}]

#Directly manipulating a variable's value means no "$", most times
incr int

#Bracing can allow you to deliniate between variable name and adjacent characters
log local0. "Today's date is the ${int}th"

全局变量

​ 全局变量用于表示存在于本地内存空间之外的变量。在 iRules 的情况下,存在超出单个会话约束的变量。例如,如果我想存储我的日志服务器的 IP 地址,并让它始终可用到通过 BIG-IP 来的每个会话,而不必每次 iRule 触发时都重新设置该变量,该怎么办?那将是一个全局变量。Tcl 有一个机制来处理这个问题,而且它相对容易使用。

​ Tcl 中的全局变量处理需要一个共享内存空间,我们在 iRules 中包含了一个新的命名空间,称为“静态”命名空间。您可以在不破坏 CMP 的情况下有效地在静态命名空间中设置静态全局变量数据,从而不会降低性能。为此,只需设置您的 static::varname 变量,可能在 RULE_INIT 中,因为该特殊事件仅在加载时运行一次,并且静态变量在范围内是全局的,这意味着它们保持设置直到重新加载配置。一旦设置了这些变量,您就可以像调用任何 iRule 中的任何其他变量一样调用它们,他们将永远可用。它看起来像这样:

1
2
3
4
5
#Set a static variable value, which will exist until config reload, living outside of the scope of any one particular iRule
set static::logserver "10.10.1.145"

#Reference that static variable just as you would any other variable from within any iRule
log $static::logserver "This is a remote log message"

4. 控制结构

​ 控制结构是一种逻辑语句,允许您进行比较,并根据比较结果执行某些操作。如果您使用过任何形式的脚本编写过它们,请使用 if、else、switch 等。这是检查值是否等于零,然后仅在为真时才执行代码块。

​ 就像没有事件一样,根据网络流的情况,您将无法在适当的时间执行代码;如果没有控制结构,您将无法在某些情况下执行代码的逻辑分离,这显然对构建功能脚本至关重要。

​ 在 iRules 中,你将使用的主要控制结构是“if”、“else”、“elseif”、“switch”和“class”。

a. if 语句

​ if语句 :只有当语句包含的结果为真时才执行下面的代码。示例:

1
2
3
if {[info exists $auth]} {
pool auth_pool
}

b. else 语句

​ 就else语句其本身而言,它并不是真正的控制结构,而是 if 语句的可选补充。如果没命中if语句,则会运行else语句 。示例:

1
2
3
4
5
if {[info exists $auth]} {
pool auth_pool
} else {
pool other_pool
}

c. elseif 语句

​ 如果我们希望能够进行多重比较并根据哪个结果被证明是正确的而采取不同的行动呢?我们需要创建一个多选逻辑语句,可能是最常见的方式是使用 elseif 语句,它也与 if 结构相关联。示例:

1
2
3
4
5
6
7
if {[info exists $auth]} {
pool auth_pool
} elseif {[info exists $secondary]} {
pool secondary_pool
} else {
default_pool
}

d. switch 语句

​ switch 语句可用于替换 if、if/else、if/elseif 链等。这是一个非常通用、紧凑的语句。switch 语句的一个重要警告是,虽然它支持多个匹配选项,但它是针对单个比较字符串的。示例:

1
2
3
4
5
6
7
8
switch [HTTP::uri] { 
“/app1” {
pool http_pool1
}
“/app2” {
pool http_pool2
}
}

​ 使用 switch 语句执行的特别有用的技巧。如果您的逻辑语句看起来像“如果 URI 是 /app1、/app2 或 /app3,则发送到 http_pool”。满足该要求的 switch 语句将非常简单:

1
2
3
4
5
6
7
switch [HTTP::uri] {
“/app1” –
“/app2” –
“/app3” {
pool http_pool
}
}

最后,通过 switch 语句,您可以使用glob 样式的模式匹配。示例:

1
2
3
4
5
6
7
8
switch -glob [HTTP::uri] {
"*\\?*ghtml*" {
#do work here
}
{*\?*ghtml*} {
#do work here
}
}

通配符示例:

1
2
3
4
*  :  匹配任意数量的任意字符。
? : 匹配任何字符的一次出现。
\X : 反斜杠在通配符中转义一个特殊字符,就像它在 Tcl 替换中所做的那样。使用反斜杠可以让您使用 glob 来匹配*或? 。
[...] : 匹配括号内任何字符的一次出现。可以通过使用括号之间的范围来匹配一系列字符。例如,[az] 将匹配任何小写字母。

e. class 语句

​ 在处理iRules的时候,有时候我们需要在iRules执行时查询一个静态数据的列表。您是否需要一些特定的客户端IP列表检查所有的传入连接请求?或许您想基于对请求的URI的不同部分的内容分配到不同的Pool中去?为了执行诸如此类的检查/动作,你就需要定义一个用来进行搜索的数据列表,而这个列表需要在不同的连接之间都可以持续存在,这就是设计Class的真正意义所在了。

​ 在F5系统中涉及到的“class”和“Data Group”有什么不同? 答:“Data Groups”是通过图形用户界面来标识,“class(es)”则是通过配置文件来标识。可能您现在有点混乱,但我向你保证,他们真的是相同的意思。

​ 在使用iRules的时候,你可以选择四种不同的Class类型:string (字符串类型)、Address(IP地址类型)、Integer(整数类型)、External File(外部文件类型)

语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
class match [<options>] <item> <operator> <class>
class search [<options>] <class> <operator> <item>
class lookup <item> <class>
class element [<options>] <index> <class>
class type <class>
class exists <class>
class size <class>
class names [-nocase] <class> [<pattern>]
class get [-nocase] <class> [<pattern>]
class startsearch <class>
class nextelement [<options>] <class> <search_id>
class anymore <class> <search_id>
class donesearch <class> <search_id>

更多class语句详细信息:https://clouddocs.f5.com/api/irules/class.html

f. 控制语句结构选择

  • 如果是最常用的原因。如果要进行 1-3 次比较,if 或 if/else(if) 可能是最合适的,也最容易使用和理解。

  • 如果您要进行 3-10 次比较,您可能应该使用 switch 语句。一般来说,它们具有更高的性能、更容易阅读和更简单的调试。

  • 进行 10-15 个条目比较,您应该使用数据组和 class 命令。

5. 常见运算符

运算符 解释
== ; equal ; eq 等于,对所有操作数类型都有效。
!= ; not equal ; ne 不等于,对所有操作数类型都有效。
< ; lt 小于,对所有操作数类型都有效。
> ; gt 大于,对所有操作数类型都有效。
<= ; le 小于等于,对所有操作数类型都有效。
>= ; ge 大于等于,对所有操作数类型都有效。
! 否定
&& ; and 逻辑与,如果两个操作数都非零,则产生 1 结果,否则产生 0。仅对布尔和数字(整数或浮点)操作数有效。
or 逻辑或。如果两个操作数都为零,则结果为 0,否则为 1。仅对布尔和数字(整数或浮点)操作数有效。
x?y:z If-then-else,如在 C 中。如果 x 计算为非零,则结果是 y 的值。否则结果是 z 的值。x 操作数必须具有布尔值或数值。
contains 测试一个字符串是否包含另一个字符串
ends_with 测试一个字符串是否以另一个字符串结尾
starts_with 测试一个字符串是否以另一个字符串开始
matches 测试一个字符串是否与另一个字符串匹配。
matches_glob 在比较中实现 glob 样式匹配。
matches_regex 测试一个字符串是否与正则表达式匹配
switch 根据给定的值评估多个脚本之一。

6. 分隔符

a. 空格

​ 在 tcl 中,基本上一切都是命令。命令有名称、选项和参数。空格(空格或制表符)用于分隔命令名称及其选项和参数,它们只是字符串。换行符或分号用于终止命令。

示例:

1
log local0. "iRule, Do you?"
Command log local0. “iRule. Do you?”
Command Name log
Command Parameter local0.
Command Parameter “iRule. Do you?”

空格字符还用于分隔 tcl 列表中的元素。

b. 双引号 “”

​ 一对双引号 " " 之间的所有内容,包括换行符和分号,都被分组并解释为一个连续的字符串。例如,最常用的 iRules 命令之一 HTTP::redirect 将完全限定的 URL 作为参数,应该用双引号引起来:HTTP::redirect "http://host.domain.com/uri"

​ 表达式中作为操作数的字符串必须用双引号或大括号括起来,以避免被解释为数学函数。

c. 方括号 []

​ 嵌套命令由方括号 [ ] 分隔,括号之间的所有内容都被视为一个命令,解释器通过丢弃括号并用嵌套命令的结果替换它们之间的所有内容来重写它嵌套的命令。(如果您编写过任何 shell 脚本,这大致相当于用反引号包围命令,尽管 tcl 允许多个嵌套。)

​ 在这个例子中,由方括号分隔的命令在最终执行之前被评估并替换到 log 命令中:

1
log local0. "[IP::client_addr]:[TCP::client_addr] requested [HTTP::uri]"

​ 当解释器试图将方括号内的字符串解释为命令时,方括号内包含的字符串不是有效的 tcl 或 iRule 命令,将产生“未定义过程”错误。

*d. 反斜杠转义 *

​ 反斜杠转义是通过在它们前面加上反斜杠来表示具有特殊含义的字符( $ {} [] " 等)。此示例生成“未定义过程:0-9”错误,因为里面的字符串“0-9” [ ] 被解释为一个命令:

1
2
if { [HTTP::uri]matches_regex "[0-9]{2,3}"} { 
TCL error: undefined procedure: 0-9 while execution "[0-9]

此示例按预期工作,因为方括号已被转义:

1
2
3
if { $urimatches_regex "\[0-9\]{2,3}" } { 
body
}

这个例子也有效,因为大括号内方括号的内容不是作为命令计算的(而且它也更具可读性):

1
2
3
if { $urimatches_regex {[0-9]{2,3}}}{ 
body
}

(上述 2 种方法在性能方面大致相当。)

​ 反斜杠替换的另一个常见用途是行继续:在多行上继续长命令。当一行以反斜杠结尾时,解释器将它和它后面的换行符以及下一行开头的所有空格转换为单个空格字符,然后将连接的行作为单个命令进行计算:

1
2
log local0. "[IP::client_addr]:[TCP::client_addr] requested [HTTP::uri] at [clock \
seconds]. Request headers were [HTTP::header names]. Method was [HTTP::method]"

e. 小括号 ()

​ iRules 中括号 ( ) 的最常见用途是执行否定比较。

正确用法:

1
2
3
4
5
6
set x xxx
if { !($x == 3) } {
log "no match"
} else {
log "match
}

错误示例:

1
2
3
4
5
6
set x xxx
if { !$x == 3 } {
log "no match"
} else {
log "match"
}

f. 大括号 {}

​ 大括号是 tcl 和 iRules 中使用最广泛且可能理解最少的分组形式。它们可能会令人困惑,因为它们用于许多不同的上下文,但基本上大括号定义了一个或多个元素的空格分隔列表,或一个或多个命令的换行分隔列表。我将尝试举例说明在 iRules 中大括号常用的每种不同方式。

  • 带列表的大括号

    ​ 列表一般是用大括号括起来的空格分隔的单词。列表和大括号一样,可以嵌套。此示例显示了一个包含 3 个元素的列表,每个元素包含一个包含 2 个元素的列表:

1
{ { {ab} {cd} } { {ef} {gh} } { {ij} {kl} } }
  • 带子命令列表的大括号

    ​ 一些命令使用大括号来分隔子命令列表。“when”命令就是一个很好的例子:在每个 iRule 中,大括号用于分隔所有 iRules 事件,封装构成每个触发事件逻辑的命令列表。每个事件声明的第一行必须以左大括号结束,最后一行必须以右大括号结束,如下所示:

1
2
3
when HTTP_REQUEST {
body
}
  • 带参数列表的大括号

    ​ 使用大括号来定义一个参数。例如,“字符串映射”命令需要一个包含替换操作的值对的参数。值对参数是作为单个参数传递的列表。

1
string map {a x b y c z} aabbcc
  • 使用大括号的特殊情况示例:

错误示例:

1
2
3
4
set var1 111
set var2 222
log local0. "$var1xxx$var2"
TCL error: can't read "var1xxx": no such variable while executing "log local0. "$var1xxx$var2""

正确示例:

1
2
3
4
5
6
set var1 111 
set var2 222
log local0。“${var1}xxx$var2

返回:
111xxx222

7. 流量引导

a. 引导流量至pool

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
when CLIENT_ACCEPTED {
if { [IP::addr [IP::client_addr] equals 10.10.10.10] } {
pool my_pool
}
}
when HTTP_REQUEST {
if { [HTTP::uri] ends_with ".gif" } {
if { [LB::status pool my_Pool member 10.1.2.200 80] eq "down" } {
log "Server $ip $port down!"
pool fallback_Pool
} else {
pool my_Pool member 10.1.2.200 80
}
}
}

b. 引导流量至node

1
2
3
4
5
when HTTP_REQUEST {
if { [HTTP::uri] ends_with ".gif" } {
node 10.1.2.200 80
}
}

c. 引导流量到virtual

1
2
3
4
5
6
7
when HTTP_REQUEST { 
# 发送请求到新的虚拟服务器
virtual my_post_processing_server
}
when HTTP_REQUEST {
log local0. “当前虚拟服务器名称:[虚拟名称]”
}

d. 重定向流量 HTTP::redirect

1
2
3
4
5
when HTTP_RESPONSE {
if { [HTTP::uri] eq “/app1?user=admin”} {
HTTP::redirect "http://www.example.com/admin"
}
}

e. 拒绝 reject

1
2
3
4
5
when CLIENT_ACCEPTED {
if { [TCP::local_port] != 443 } {
reject
}
}

f. 丢弃 drop & discard

1
2
3
4
5
6
when SERVER_CONNECTED {
if { [IP::addr [IP::client_addr] equals 10.1.1.80] } {
discard
log local0. "connection discarded from [IP::client_addr]"
}
}

8. 事件

​ iRules 是一种事件驱动语言。当流量命中事件时,会基于事件调用事件下的代码段。

a. 事件触发顺序:

irule.png