协议扩展 - 用例

协议扩展可用于以下用例。

  • 基于消息的负载平衡 (MBLB)
  • 流媒体
  • 基于令牌的负载均衡
  • 负载平衡持久性
  • 基于 TCP 连接的负载平衡
  • 基于内容的负载均衡
  • SSL
  • 修改流量
  • 将流量发送到客户端或服务器
  • 连接建立的处理数据

基于消息的负载平衡

协议扩展支持基于消息的负载平衡 (MBLB),它可以解析 Citrix ADC 设备上的任何协议,并对到达一个客户端连接的协议消息进行负载平衡,即通过多个服务器连接分发消息。MBLB 是通过解析客户端 TCP 数据流的用户代码实现的。

TCP 数据流传递给 on_data 回调以便用于客户端和服务器行为。TCP 数据流可以通过 Lua 字符串(如接口)提供给扩展函数。您可以使用类似于 Lua 字符串 API 的 API 来解析 TCP 数据流。

有用的 API 包括:

data:len()

data:find()

data:byte()

data:sub()

data:split()

将 TCP 数据流解析为协议消息后,用户代码只需将协议消息发送到下一个可用的上下文,即可实现负载平衡,从传递给客户端 on_data 回调的上下文。

ns.send() API 用于将消息发送到其他处理模块。除了目标上下文之外,发送 API 还将事件名称和可选负载作为参数。事件名称和行为的回调函数名称之间存在一对一的对应关系。事件的回调在 上调用 <event_name>。回调名称仅使用小写。

例如,TCP 客户端和服务器 on_data 回调是名为“DATA”的事件的用户定义处理程序。对于在一次发送调用中发送整个协议消息,使用 EOM 事件。EOM 代表消息结束,表示协议消息到 LB 上下文流的结束,因此对于此消息后面的数据,将做出新的负载平衡决策。

扩展代码有时可能不会在 on_data 事件中收到整个协议消息。在这种情况下,数据可以通过使用 ctxt: hold () API 来保存。保留 API 可用于 TCP 客户端和服务器回调上下文。当调用“保持数据”时,数据存储在上下文中。当在同一上下文中接收到更多数据时,新接收的数据会附加到先前存储的数据,并且使用组合的数据再次调用 on_data 回调函数。

注意: 使用的负载平衡方法取决于对应于负载平衡上下文的负载平衡虚拟服务器的配置。

以下代码段显示了使用发送 API 发送已解析的协议消息。

示例:

    function client.on_data(ctxt, payload)
        --
        -- code to parse payload.data into protocol message comes here
        --
        -- sending the message to lb
        ns.send(ctxt.output, "EOM", {data = message})
    end -- client.on_data

    function server.on_data(ctxt, payload)
        --
        -- code to parse payload.data into protocol message comes here
        --
        -- sending the message to client
        ns.send(ctxt.output, "EOM", {data = message})

    end -- server.on_data
<!--NeedCopy-->

流媒体

在某些情况下,可能不需要保留 TCP 数据流直到收集整个协议消息。事实上,除非需要,否则不建议。保存数据会增加 Citrix ADC 设备上的内存使用量,并会使设备容易受到 DDoS 攻击,因为许多连接上具有不完整的协议消息的 Citrix ADC 设备上的内存。

用户可以通过使用发送 API 在扩展回调处理程序中实现 TCP 数据流。数据可以分块发送,而不是在收集整个消息之前保存数据。通过使用 DATA 事件将数据发送到 ctxt.output 会发送部分协议消息。它可以跟随更多的数据事件。必须发送 EOM 事件以标记协议消息的结束。下游的负载平衡上下游会对接收的第一个数据做出负载平衡决策。在收到 EOM 消息后,将做出新的负载平衡决策。

要流式传输协议消息数据,请发送多个 DATA 事件,后跟一个 EOM 事件。连续的 DATA 事件和以下 EOM 事件发送到顺序中第一个 DATA 事件的负载平衡决策所选的相同服务器连接。

对于发送到客户端上下文, EOM 和 DATA 事件实际上是相同的, 因为有由客户端上下游 EOM 事件没有特殊处理.

基于令牌的负载均衡

对于本地支持的协议,Citrix ADC 设备支持使用 PI 表达式创建令牌的基于令牌的负载平衡方法。对于扩展,协议事先未知,因此不能使用 PI 表达式。对于基于令牌的负载平衡,您必须将默认负载平衡虚拟服务器设置为使用 USER_TOKEN 负载平衡方法,并通过使用 user_token 字段调用发送 API 来从扩展代码中提供令牌值。如果令牌值是从发送 API 发送的,并且在默认负载平衡虚拟服务器上配置了 USER_TOKEN 负载平衡方法,则通过基于令牌值计算哈希来做出负载平衡决策。令牌值的最大长度为 64 字节。

add lb vserver v\_mqttlb USER\_TCP –lbMethod USER\_TOKEN

以下示例中的代码段使用发送 API 发送 LB 令牌值。

示例:

        -- send the message to lb




        -- user_token is set to do LB based on clientID




        ns.send(ctxt.output, "EOM", {data = message,

                                 user_token = token_info})
<!--NeedCopy-->

负载平衡持久性

负载平衡持久性与基于令牌的负载平衡密切相关。用户必须能够以编程方式计算持久性会话值并将其用于负载平衡持久性。发送 API 用于发送持久性参数。要使用负载平衡持久性,您必须在默认负载平衡虚拟服务器上设置 USERSISE 持久性类型,并通过使用 user_session 字段调用发送 API 来从扩展代码中提供持久性参数。持久化参数值的最大长度为 64 字节。

如果自定义协议需要多种类型的持久性,则必须定义用户持久性类型并对其进行配置。用于配置虚拟服务器的参数名称由协议实现者决定。参数的配置值也可用于扩展代码。

以下 CLI 和代码段显示了使用发送 API 来支持负载平衡持久性。 mqtt.lua 的代码清单部分中的代码清单 还说明了 user_session 字段的使用情况。

对于持久性,您必须在负载平衡虚拟服务器上指定 USERSISE 持久性类型,并从 ns.send API 传递 user_session 值。

add lb vserver v\_mqttlb USER\_TCP –persistencetype USERSESSION

将 MQTT 消息发送到负载平衡器,并在负载中将 user_session 字段设置为 ClientiID。

示例:

-- send the data so far to lb

-- user_session is set to clientID as well (it will be used to persist session)

ns.send(ctxt.output, “DATA”, {data = data, user_session = clientID})
<!--NeedCopy-->

基于 TCP 连接的负载平衡

对于某些协议,可能不需要 MBLB。相反,您可能需要基于 TCP 连接的负载平衡。例如,MQTT 协议必须解析 TCP 流的初始部分,以确定用于负载平衡的令牌。此外,同一 TCP 连接上的所有 MQTT 消息必须发送到同一服务器连接。

基于 TCP 连接的负载平衡可以通过使用仅包含 DATA 事件的发送 API 而不发送任何 EOM 来实现。这样,下游负载平衡上下文将根据首先接收的数据作为负载平衡决策的基础,并将所有后续数据发送到负载平衡决策所选的同一服务器连接。

此外,某些用例可能需要在做出负载平衡决策后绕过扩展处理的能力。绕过扩展调用会提高性能,因为流量纯粹由本机代码处理。绕过可以通过使用 ns.pipe()API 来完成。调用 pipe () API 扩展代码可以将输入上下文连接到输出上下文。调用 pipe()后,来自输入上下文的所有事件直接转到输出上下文。有效地,从管道中移除了进行 pipe () 调用的模块。

以下代码段显示了流式处理和使用 pipe () API 绕过模块的情况。 mqtt.lua 的代码清单部分中的 代码清单还说明了如何进行流媒体以及如何使用 pipe () API 绕过该模块以获取连接中的其余流量。

示例:

        -- send the data so far to lb
        ns.send(ctxt.output, "DATA", {data = data,
                                       user_token = clientID})
        -- pipe the subsequent traffic to the lb - to bypass the client on_data handler
        ns.pipe(ctxt.input, ctxt.output)
<!--NeedCopy-->

基于内容的负载均衡

对于本机协议,支持像协议扩展功能一样的内容切换。使用此功能,您可以将数据发送到所选负载平衡器,而不是将数据发送到默认负载平衡器。

通过使用 ctxt: lb_connect (<lbname>) API 来实现协议扩展的内容切换功能。此 API 可用于 TCP 客户端上下文。使用此 API,扩展代码可以获取与已配置的负载平衡虚拟服务器相对应的负载平衡上下文。然后,您可以将发送 API 与由此获得的负载平衡上下文一起使用。

lb 上下文有时可以为 NULL:

  • 虚拟服务器不存在
  • 虚拟服务器不是用户协议类型
  • 虚拟服务器的状态不为 UP
  • 虚拟服务器是用户虚拟服务器,而不是负载平衡虚拟服务器

如果在使用目标负载平衡虚拟服务器时删除该服务器,则与该负载平衡虚拟服务器关联的所有连接将重置。

以下代码段显示了 lb_connect () API 的使用。代码将客户端 ID 映射到使用 Lua 表 lb_map 对虚拟服务器名称 (lbname) 进行负载平衡,然后使用 lb_connect () 获取 lbname 的 LB 上下文。最后使用发送 API 发送到 LB 上下文。

    local lb_map = {
       ["client1*"] = "lb_1",
       ["client2*"] = "lb_2",
       ["client3*"] = "lb_3",
       ["client4*"] = "lb_4"
    }

    -- map the clientID to the corresponding LB vserver and connect to it
    for client_pattern, lbname in pairs(lb_map) do
       local match_idx = string.find(clientID, client_pattern)
       if (match_idx == 1) then
      lb_ctxt = ctxt:lb_connect(lbname)
      if (lb_ctxt == nil) then
         error("Failed to connect to LB vserver: " .. lbname)
      end
      break
       end
    end
    if (lb_ctxt == nil) then
    -- If lb context is NULL, the user can raise an error or send data to default LB
       error("Failed to map LB vserver for client: " .. clientID)
    end
-- send the data so far to lb
ns.send(lb_ctxt, "DATA", {data = data}
<!--NeedCopy-->

SSL

支持使用扩展的协议的 SSL,方式类似于支持本机协议的 SSL。使用相同的解析代码创建自定义协议,您可以通过 TCP 或 SSL 创建协议实例,然后可以使用该实例来配置虚拟服务器。同样,您可以通过 TCP 或 SSL 添加用户服务。

有关更多信息,请参阅 为 MQTT 配置 SSL 卸载和使用端到端加密为 MQTT 配置 SSL 卸载。

服务器连接复用

有时,客户端一次发送一个请求,只有在从服务器收到第一个请求的响应后才发送下一个请求。在这种情况下,服务器连接可以重复用于其他客户端连接,以及在同一连接上的下一条消息,在响应发送到客户端之后。要允许其他客户端连接重复使用服务器连接,您必须在服务器端上下文中使用 ctxt: reuse_server_连接() API。

注意:此 API 在 Citrix ADC 12.1 版本 49.xx 及更高版本中可用。

修改流量

要修改请求或响应中的数据,必须使用使用高级策略 PI 表达式的本机重写功能。由于您不能在扩展中使用 PI 表达式,因此您可以使用以下 API 修改 TCP 流数据。

data:replace(offset, length, new_string)
data:insert(offset, new_string)
data:delete(offset, length)
data:gsub(pattern, replace [,n]))

以下代码段显示了 replace () API 的使用。

-- Get the offset of the pattern, we want to replace
   local old_pattern = “pattern to repalace”
local old_pattern_length = old_pattern:len()
   local pat_off, pat_end = data:find(old_pattern)
   -- pattern is not present
if (not pat_off) then
    goto send_data
   end
  -- If the data we want to modify is not completely present, then
  -- wait for more data
  if (not pat_end) then
        ctxt:hold(data)
        data = nil
     goto done
  end
data:replace(pat_off, old_pattern_length, “new pattern”)
::send_data::
ns.send(ctxt.output, “EOM”, {data = data})
::done::

以下代码段显示了插入 () API 的使用。

data:insert(5, “pattern to insert”)

下面的代码片段显示了插入()API 的使用,当我们想在某个模式之后或之前插入:

-- Get the offset of the pattern, after or before which we want to insert
   local pattern = “pattern after/before which we need to insert”
local pattern_length = pattern:len()
   local pat_off, pat_end = data:find(pattern)
-- pattern is not present
   if (not pat_off) then
    goto send_data
   end
  -- If the pattern after which we want to insert is not
  -- completely present, then wait for more data
  if (not pat_end) then
        ctxt:hold(data)
        data = nil
     goto done
  end
-- Insert after the pattern
data:insert(pat_end + 1, “pattern to insert”)
   -- Insert before the pattern
data:insert(pat_off, “pattern to insert”)
::send_data::
    ns.send(ctxt.output, “EOM”, {data = data})
::done::

以下代码段显示了删除 () API 的使用。

-- Get the offset of the pattern, we want to delete
   local delete_pattern = “pattern to delete”
local delete_pattern_length = delete_pattern:len()
   local pat_off, pat_end = data:find(old_pattern)
   -- pattern is not present
if (not pat_off) then
          goto send_data
   end
  -- If the data we want to delete is not completely present,
  -- then wait for more data
  if (not pat_end) then
        ctxt:hold(data)
        data = nil
    goto done
  end
data:delete(pat_off, delete_pattern_length)
::send_data::
ns.send(ctxt.output, “EOM”, {data = data})
::done::

以下代码段显示了 gsub () API 的使用。

    -- Replace all the instances of the pattern with the new string
data:gsub(“old pattern”, “new string”)
-- Replace only 2 instances of “old pattern”
data:gsub(“old pattern”, “new string”, 2)
-- Insert new_string before all instances of “http”
data:gsub(“input data”, “(http)”, “new_string%1”)
-- Insert new_string after all instances of “http”
data:gsub(“input data”, “(http)”, “%1new_string”)
-- Insert new_string before only 2 instances of “http”
data:gsub(“input data”, “(http)”, “new_string%1”, 2)

注意:此 API 在 Citrix ADC 12.1 版本 50.xx 及更高版本中可用。

将流量发送到客户端或服务器

您可以使用 ns.send () API 将源自扩展代码的数据发送到客户端和后端服务器。要直接与客户端发送或接收响应,从客户端上下文,您必须使用 ctxt.client 作为目标。要从服务器上下文直接通过后端服务器发送或接收响应,必须使用 ctxt.server 作为目标。负载中的数据可以是 TCP 流数据或 Lua 字符串。

要停止连接上的流量处理,您可以从客户端或服务器上下文使用 ctxt: close () API。此 API 关闭客户端连接或链接到它的任何服务器连接。

当您调用 ctxt: close () API 时,扩展代码会向客户端和服务器连接发送 TCP FIN 数据包,如果在此连接上从客户端或服务器收到更多数据,则设备会重置连接。

以下代码段显示了 ctxt.client 和 ctxt: close () API 的使用。

    -- If the input packet is not MQTT CONNECT type, then
-- send some error response to the client.
function client.on_data(ctxt, payload)
    local data = payload.data
    local offset = 1
    local msg_type = 0
    local error_response = “Missing MQTT Connect packet.”
    byte = data:byte(offset)
msg_type = bit32.rshift(byte, 4)
if (msg_type ~= 1) then
-- Send the error response
   ns.send(ctxt.client, “DATA”, {data = error_response})
-- Since error response has been sent, so now close the connection
    ctxt:close()
end

以下代码段显示了用户可以在正常流量流中注入数据的示例。

-- After sending request, send some log message to the server.
function client.on_data(ctxt, payload)
local data = payload.data
local log_message = “client id : “..data:sub(3, 7)..” user name : “ data:sub(9, 15)
-- Send the request we get from the client to backend server
ns.send(ctxt.output, “DATA”, {data = data})
After sending the request, also send the log message
ns.send(ctxt.output, “DATA”, {data = log_message”})
end

以下代码段显示了 ctxt.to_server API 的使用。

-- If the HTTP response status message is “Not Found”,
-- then send another request to the server.
function server.on_data(ctxt, payload)
    local data = payload.data
    local request “GET /default.html HTTP/1.1\r\n\r\n”ss
    local start, end = data:find(“Not Found”)
    if (start) then
    -- Send the another request to server
        ns.send(ctxt.server, “DATA”, {data = request})
end

注意:此 API 在 Citrix ADC 12.1 版本 50.xx 及更高版本中可用。

连接建立的数据处理

可能存在一个用例,您希望在连接建立时发送一些数据(当接收最终 ACK 时)。例如,在代理协议中,您可能希望在建立连接时将客户端的源和目标 IP 地址和端口发送到后端服务器。在这种情况下,您可以使用 client.init () 回调处理程序发送连接建立时的数据。

下面的代码片段显示了对 client.init () 回调的使用:

-- Send a request to the next processing context
-- on the connection establishment.
function client.init(ctxt)
   local request “PROXY TCP4” + ctxt.client.ip.src.to_s + “ “ + ctxt.client.ip.dst.to_s + “ “ + ctxt.client.tcp.srcport + “ “ +     ctxt.client.tcp.dstport
-- Send the another request to server
   ns.send(ctxt.output, “DATA”, {data = request})
  end

注意:此 API 在 Citrix ADC 13.0 版本 xx.xx 及更高版本中可用。