可编程流防护 (Programmable Flow Protection) 是一种 DDoS 防护系统,可针对基于自定义或标准第 7 层 UDP 的协议(例如游戏协议、金融服务协议、VoIP、电信和流媒体)防范 DDoS 攻击。在拓扑结构方面,它同时支持非对称和对称配置,但仅审查入口 (ingress) 流量。
可编程流防护目前处于非公开测试 (closed beta) 阶段,仅作为 Magic Transit(BYOIP 或 Cloudflare 租用的 IP)服务的附加项提供。如果您想启用该系统,请联系您的账户团队或填写此表单 ↗。
可编程流防护系统允许您在全球 Cloudflare Anycast 网络中,编写并运行您自己的 C 语言数据包层有状态程序,这些程序作为运行在用户空间的扩展伯克利数据包过滤器 (eBPF) 程序。一个 eBPF 程序 ↗ 是一个数据包过滤系统,允许开发人员编写高性能的自定义网络逻辑。
可编程流防护检查并解析基于 UDP 的应用程序协议(深度包检测 - DPI),并根据您的程序确定数据包的处理结果。使用您的自定义程序逻辑,您可以放行经授权的用户,同时主动阻止攻击。
该系统构建在 flowtrackd 平台之上,这是 Cloudflare 的有状态缓解平台。可编程流防护系统依赖于 DDoS 高级防护系统的常规设置来运行。它遵循您选择路由通过高级防护系统的前缀,以及允许列表。高级 DDoS 防护系统必须被启用才能让可编程流防护系统正常工作。
在测试阶段,Cloudflare 将协助并指导用户编写他们自己的代码。针对流行游戏协议和 VoIP 协议的开箱即用代码片段(模板)可能会在稍后提供。
在您的账户启用可编程流防护后,在 Cloudflare 仪表板中前往 Networking(网络) > L3/4 DDoS Protection(L3/4 DDoS 防护) > Advanced Protection(高级防护)。在 Programmable Flow Protection(可编程流防护) 选项卡内:
-
上传您用 C 编写的 eBPF 程序。
系统会校验该程序并将其存储在您的账户中。API 会编译该程序,然后针对编译后的程序运行校验器,以强制执行内存检查并验证程序能够终止。如果程序编译或校验失败,Cloudflare 仪表板将返回详细的错误消息。
-
创建规则
-
要观察程序的行为,请检查 Network Analytics 仪表板并选择 Programmable Flow Protection(可编程流防护) 选项卡。
您可以创建具有不同规则设置的其他规则,这些规则可以范围限定到各种区域和 Cloudflare 位置,以更改其模式(缓解或监控),从而适应您的流量模式和业务用例。
可编程流防护系统支持 Data Localization 套件。
以下步骤演示了如何编写一个示例程序,该程序丢弃所有带有 IPv6 请求头的用户数据报协议 (UDP) 流量。它还会丢弃发送到端口 66 的流量,以及在 UDP 负载中不包含特定自定义应用请求头值的流量。
-
添加一个 define 指令来指定所使用的 helper 函数版本。
随着 Cloudflare 向可编程流防护 API 添加更多功能,我们将发布新版本的 API。各版本保证向后兼容。
#define CF_EBPF_HELPER_V0 -
引入 Cloudflare eBPF 头文件。
这些文件包含 helper 函数,用于将输入的包裹数据解析并传递给 BPF 程序。
#include <cf_ebpf_defs.h> #include <cf_ebpf_helper.h> -
定义用于数据包处理的入口函数。
您的程序必须具有以下确切的函数签名,才能妥善通过 Cloudflare 的程序校验。
返回类型
uint64_t决定了 Cloudflare 是放行还是丢弃数据包。函数名cf_ebpf_main用作程序的入口点。参数void *state是指 Cloudflare 提供给您的 BPF 程序的输入数据。uint64_t cf_ebpf_main(void *state) -
将输入参数转换为可用的结构体。
将输入数据转换为
cf_ebpf_generic_ctx,它告诉 Cloudflare 我们读取的内存中的数据边界。然后,声明用于数据解析的变量。
cf_ebpf_parsed_headers将包含 IPv4、IPv6 和 UDP 请求头。cf_ebpf_packet_data将持有 Cloudflare 接收到的原始 IP 数据包的副本(最大 1500 字节),以及数据包长度和 IP 请求头长度。struct cf_ebpf_generic_ctx *ctx = state; struct cf_ebpf_parsed_headers headers; struct cf_ebpf_packet_data *p; -
通过调用 helper 函数填充变量。
您必须通过调用
parse_packet_data这一 helper 函数来填充变量,Cloudflare 已在步骤 2 中包含的头文件中提供了该函数。parse_packet_data函数执行通过程序校验器所需的内存检查。parse_packet_data函数在成功时返回0。如果成功,输入参数将被正确填充。parse_packet_data函数在失败时返回1。如果parse_packet_data失败,程序必须返回CF_EBPF_DROP丢弃数据包,以通过校验器。if (parse_packet_data(ctx, &p, &headers) != 0) { return CF_EBPF_DROP; }解析成功后的可用值:
struct cf_ebpf_packet_data { /* 数据包的总长度。 */ size_t total_packet_length; /* IP 请求头的大小。支持 IPv4(包括选项)和 IPv6。 */ size_t ip_header_length; /* 数据包的字节,从 IP 请求头开始。 */ uint8_t packet_buffer[1500]; }; struct cf_ebpf_parsed_headers { /* 指向解析后的 IPv4 请求头的指针,如果存在(否则为 null)。 */ struct iphdr *ipv4; /* 指向解析后的 IPv6 请求头的指针,如果存在(否则为 null)。 */ struct ipv6hdr *ipv6; /* 指向解析后的 UDP 请求头的指针。 */ struct udphdr *udp; /* 指向数据包上下文数据最后一个有效字节的原始指针。 */ uint8_t *data_end; };有关 helper 函数和结构体的完整定义,请参阅 BPF helper 函数和结构体。
-
编写您的自定义逻辑。
先前的步骤已经确立了对您编写的任何程序来说都是相同的代码,无论其逻辑如何。
现在,您可以编写您自己的自定义逻辑了。
在下方的示例片段中,程序将丢弃存在 IPv6 请求头,或者 UDP 目标端口为 66 的任何数据包。
然后,它将检查 UDP 负载中的应用请求头值,并验证其最后一个字节是否为固定值
0xCF。struct ipv6hdr *ipv6_hdr; struct udphdr *udp_hdr; ipv6_hdr = (struct ipv6hdr *)headers.ipv6; if (ipv6_hdr != NULL) { return CF_EBPF_DROP; } udp_hdr = (struct udphdr *)headers.udp; if (ntohs(udp_hdr->dest) == 66) { return CF_EBPF_DROP; } struct apphdr *app = (struct apphdr *)(udp_hdr + 1); if ((uint8_t *)(app + 1) > headers.data_end) { return CF_EBPF_DROP; } // 校验器有一个特殊的限制,它不允许超过 65535 的偏移量。 // 我们需要这一检查 (token_len > 64000) 来满足这一要求, // 尽管这是不可能的。 uint16_t token_len = app->length; if (token_len > 64000) { return CF_EBPF_DROP; } if ((uint8_t *)(app->token + token_len) > headers.data_end) { return CF_EBPF_DROP; } uint8_t *last_byte = app->token + token_len - 1; if (*last_byte != 0xCF) { return CF_EBPF_DROP; } -
对于没有被程序逻辑丢弃的任何数据包,通过返回
CF_EBPF_PASS来放行。目前支持的返回值是:
CF_EBPF_PASS = 返回值 0CF_EBPF_DROP = 返回值 1
当您将程序上传到 API 时运行的校验器,将强制要求程序仅返回已知的数值类型。
return CF_EBPF_PASS;
作为参考,下面的示例是完整的、基础的程序:
#define CF_EBPF_HELPER_V0
#include <cf_ebpf_defs.h>
#include <cf_ebpf_helper.h>
struct apphdr {
uint8_t version;
uint16_t length; // 可变长度令牌的长度
unsigned char token[0]; // 可变长度令牌
} __attribute__((packed));
uint64_t
cf_ebpf_main(void *state)
{
struct cf_ebpf_generic_ctx *ctx = state;
struct cf_ebpf_parsed_headers headers;
struct cf_ebpf_packet_data *p;
if (parse_packet_data(ctx, &p, &headers) != 0) {
return CF_EBPF_DROP;
}
struct ipv6hdr *ipv6_hdr;
struct udphdr *udp_hdr;
ipv6_hdr = (struct ipv6hdr *)headers.ipv6;
if (ipv6_hdr != NULL) {
return CF_EBPF_DROP;
}
udp_hdr = (struct udphdr *)headers.udp;
if (ntohs(udp_hdr->dest) == 66) {
return CF_EBPF_DROP;
}
struct apphdr *app = (struct apphdr *)(udp_hdr + 1);
if ((uint8_t *)(app + 1) > headers.data_end) {
return CF_EBPF_DROP;
}
// 校验器有一个特殊的限制,它不允许超过 65535 的偏移量。
// 我们需要这一检查 (token_len > 64000) 来满足这一要求,
// 尽管这是不可能的。
uint16_t token_len = app->length;
if (token_len > 64000) {
return CF_EBPF_DROP;
}
if ((uint8_t *)(app->token + token_len) > headers.data_end) {
return CF_EBPF_DROP;
}
uint8_t *last_byte = app->token + token_len - 1;
if (*last_byte != 0xCF) {
return CF_EBPF_DROP;
}
return CF_EBPF_PASS;
}下面的示例程序使用 helper 函数来实现基于 UDP 的质询响应机制,以在来自相同源 IP 的数据包之间维护状态。通过要求客户端在放行其流量之前,证明它们能够接收并响应质询,这对于缓解 DDoS 攻击非常有用。
质询机制的工作原理如下:
当一个数据包来自未知的源 IP 时,程序生成一个包含随机 nonce 的质询数据包,并在状态表中将该源 IP 标记为“已质询 (challenged)”。原始数据包会被丢弃。
如果一个数据包来自已经被质询的源 IP,程序将检查该数据包是否包含正确的质询响应(nonce 与密钥值进行异或 - XOR)。如果响应正确,源 IP 将被标记为“已校验 (verified)”。如果响应错误,源 IP 会被立即加入黑名单。
来自已校验源 IP 的数据包将不经进一步检查直接通过。
-
包含 Cloudflare eBPF 头文件并定义 helper 版本。
#define CF_EBPF_HELPER_V0 #include <cf_ebpf_defs.h> #include <cf_ebpf_helper.h> -
定义质询响应协议的常量。
质询响应是通过将 nonce 与一个 secret 密钥值进行 XOR 计算出来的。过期时间决定了被质询或已验证状态保持有效的时长。
#define CHALLENGE_SECRET 0xDEADBEEFCAFEBABEULL #define CHALLENGE_EXPIRY_SECS 60 #define VERIFIED_EXPIRY_SECS 3600 -
定义质询数据包的结构体。
质询数据包包含客户端必须对其做出响应的 nonce,以及用于客户端响应的空间。
struct challenge_packet { uint64_t nonce; // 用于此质询的随机 nonce uint64_t response; // 预期值:nonce XOR CHALLENGE_SECRET }; -
定义入口函数并解析数据包。
uint64_t cf_ebpf_main(void *state) { struct cf_ebpf_generic_ctx *ctx = state; struct cf_ebpf_parsed_headers headers; struct cf_ebpf_packet_data *p; if (parse_packet_data(ctx, &p, &headers) != 0) { return CF_EBPF_DROP; } struct udphdr *udp_hdr = headers.udp; -
使用
get_src_ip_status检查源 IP 的状态。该状态指出该源 IP 是新的、已被质询、已校验还是已被加入黑名单。过期时间戳表示状态何时过期。
uint8_t status; uint64_t expiry; int ret = get_src_ip_status(&status, &expiry); // 检查状态是否已过期 int64_t now = timestamp(); if (ret == 0 && expiry > 0 && (uint64_t)now > expiry) { // 状态已过期,视为新连接 ret = -1; } -
处理已校验的源 IP。
可编程流防护平台将在调用程序之前,丢弃来自黑名单 IP 的数据包。无需在程序中显式处理黑名单情况。
如果源 IP 已被验证(通过了之前的质询),则放行该数据包。
if (ret == 0 && status == CF_EBPF_SRC_IP_STATUS_VERIFIED) { return CF_EBPF_PASS; } -
检查这是否是来自已被质询源 IP 的质询响应。
如果该源 IP 之前已被质询,检查当前数据包是否包含有效的质询响应。如果响应正确,将源 IP 标记为已验证。如果响应错误,立即将源 IP 加入黑名单。
if (ret == 0 && status == CF_EBPF_SRC_IP_STATUS_CHALLENGED) { // 从用户数据中获取存储的 nonce uint64_t stored_nonce; if (get_src_ip_data(&stored_nonce) != 0) { return CF_EBPF_DROP; } // 从数据包有效负载中解析质询响应 struct challenge_packet *resp = (struct challenge_packet *)(udp_hdr + 1); if ((uint8_t *)(resp + 1) > headers.data_end) { return CF_EBPF_DROP; } // 验证响应:应当为 nonce XOR secret uint64_t expected_response = stored_nonce ^ CHALLENGE_SECRET; if (resp->response == expected_response) { // 响应正确 - 标记为已校验 set_src_ip_status(CF_EBPF_SRC_IP_STATUS_VERIFIED, VERIFIED_EXPIRY_SECS); set_src_ip_data(0); // 清除 nonce return CF_EBPF_PASS; } // 响应错误 - 立即加入黑名单 set_src_ip_status(CF_EBPF_SRC_IP_STATUS_BLOCKLISTED, 0); return CF_EBPF_DROP; } -
针对新的源 IP 发起新的质询。
生成一个随机 nonce,将其存储在状态表中,创建一个质询数据包,并使用
set_challenge发送。// 为该源 IP 生成一个新的质询 uint64_t nonce = rand(); // 存储 nonce 并标记为已质询 set_src_ip_status(CF_EBPF_SRC_IP_STATUS_CHALLENGED, CHALLENGE_EXPIRY_SECS); set_src_ip_data(nonce); // 构建要发回的质询数据包 struct challenge_packet challenge; challenge.nonce = nonce; challenge.response = 0; // 客户端将填充此字段 // 设置质询数据包缓冲区 set_challenge((uint8_t *)&challenge, sizeof(challenge)); // 丢弃原始数据包,直到客户端响应质询 return CF_EBPF_DROP; }
作为参考,以下是完整的复杂程序:
#define CF_EBPF_HELPER_V0
#include <cf_ebpf_defs.h>
#include <cf_ebpf_helper.h>
// 质询响应协议常量
#define CHALLENGE_SECRET 0xDEADBEEFCAFEBABEULL
#define CHALLENGE_EXPIRY_SECS 60
#define VERIFIED_EXPIRY_SECS 3600
// 质询数据包结构
struct challenge_packet {
uint64_t nonce;
uint64_t response;
};
uint64_t cf_ebpf_main(void *state)
{
struct cf_ebpf_generic_ctx *ctx = state;
struct cf_ebpf_parsed_headers headers;
struct cf_ebpf_packet_data *p;
if (parse_packet_data(ctx, &p, &headers) != 0) {
return CF_EBPF_DROP;
}
struct udphdr *udp_hdr = headers.udp;
// 检查源 IP 状态
uint8_t status;
uint64_t expiry;
int ret = get_src_ip_status(&status, &expiry);
// 检查状态是否已过期
int64_t now = timestamp();
if (ret == 0 && expiry > 0 && (uint64_t)now > expiry) {
ret = -1; // 视为新连接
}
// 处理已验证的源 IP - 放行
if (ret == 0 && status == CF_EBPF_SRC_IP_STATUS_VERIFIED) {
return CF_EBPF_PASS;
}
// 处理已被质询的源 IP - 检查有效响应
if (ret == 0 && status == CF_EBPF_SRC_IP_STATUS_CHALLENGED) {
uint64_t stored_nonce;
if (get_src_ip_data(&stored_nonce) != 0) {
return CF_EBPF_DROP;
}
// 从数据包负载中解析质询响应
struct challenge_packet *resp = (struct challenge_packet *)(udp_hdr + 1);
if ((uint8_t *)(resp + 1) > headers.data_end) {
return CF_EBPF_DROP;
}
// 使用 XOR 检查响应
uint64_t expected_response = stored_nonce ^ CHALLENGE_SECRET;
if (resp->response == expected_response) {
// 响应正确 - 标记为已验证
set_src_ip_status(CF_EBPF_SRC_IP_STATUS_VERIFIED, VERIFIED_EXPIRY_SECS);
set_src_ip_data(0);
return CF_EBPF_PASS;
}
// 响应错误 - 立即加入黑名单
set_src_ip_status(CF_EBPF_SRC_IP_STATUS_BLOCKLISTED, 0);
return CF_EBPF_DROP;
}
// 新源 IP - 发起初始质询
uint64_t nonce = rand();
set_src_ip_status(CF_EBPF_SRC_IP_STATUS_CHALLENGED, CHALLENGE_EXPIRY_SECS);
set_src_ip_data(nonce);
struct challenge_packet challenge;
challenge.nonce = nonce;
challenge.response = 0;
set_challenge((uint8_t *)&challenge, sizeof(challenge));
return CF_EBPF_DROP;
}该程序展示了几个核心概念:
- 状态管理:使用
get_src_ip_status、set_src_ip_status、get_src_ip_data和set_src_ip_data来追踪每个源 IP 的质询状态。 - 质询发射:使用
set_challenge发送回客户端一个质询数据包。 - 密码学验证:使用共享密钥验证客户端是否正确响应了质询。
- 过期处理:使用时间戳来让陈旧的状态条目过期。
下面的示例程序实现了一个使用固定窗口算法的每个源 IP 的速率限制器。通过限制单个源 IP 在一个时间窗口内可以发送的数据包数量,这对于缓解体积型 DDoS 攻击非常有用。
速率限制机制工作原理如下:
当一个数据包到达时,程序会检索为该源 IP 存储的状态。该状态包含了一个窗口开始时间戳和一个数据包计数器,打包在单个 64 位值中。如果当前时间仍在窗口内,计数器会递增。如果计数器超过了配置的限制,数据包将被丢弃。当窗口过期时,计数器会重置。
-
包含 Cloudflare eBPF 头文件并定义 helper 版本。
#include <cf_ebpf_defs.h> #define CF_EBPF_HELPER_V0 #include <cf_ebpf_helper.h> -
定义速率限制配置的常量。
RATE_LIMIT设置每个窗口允许的最大数据包数量。WINDOW_SECONDS定义每个时间窗口的持续秒数。#define RATE_LIMIT 100 // 每个窗口允许的最大数据包数 #define WINDOW_SECONDS 60 // 时间窗口秒数 -
定义打包和解包状态数据的宏。
源 IP 状态表为每个源 IP 存储一个
u64值。要同时跟踪时间戳和计数器,需要将它们打包到此值中:时间戳存放在高 32 位,计数器存放在低 32 位。#define PACK_STATE(ts, count) (((uint64_t)(ts) << 32) | ((uint64_t)(count) & 0xFFFFFFFF)) #define UNPACK_TIMESTAMP(data) ((uint32_t)((data) >> 32)) #define UNPACK_COUNTER(data) ((uint32_t)((data) & 0xFFFFFFFF)) -
定义入口函数并获取当前时间戳。
如果获取时间戳的 helper 失败,放行该数据包以避免误报。
uint64_t cf_ebpf_main(void *state) { // 获取当前时间戳 int64_t now = timestamp(); if (now < 0) { return CF_EBPF_PASS; // 如果获取时间戳失败,则放行数据包 } uint32_t now_secs = (uint32_t)now; -
检索该源 IP 的现有状态。
使用
get_src_ip_data来查找此源 IP 之前是否被记录过。// 尝试获取该源 IP 的现有状态 uint64_t data; int ret = get_src_ip_data(&data); uint32_t window_start; uint32_t counter; -
处理这是个新源 IP 的情况。
如果没有现有的条目(返回值为
-1),这是来自该源 IP 的第一个数据包。初始化窗口自现在开始,计数器为 1。if (ret == -1) { // 无现有条目 - 首次来自该 IP 的数据包 // 初始化:窗口从现在开始,计数器 = 1 window_start = now_secs; counter = 1; } -
处理现有的源 IP 并检查时间窗口。
如果存在条目,解包存储的时间戳和计数器。如果窗口已过期,重置这两个值。否则,递增计数器并检查是否超过速率限制。
} else if (ret != 0) { // 如果获取 src_ip_data 出现其他未知错误,放行数据包 return CF_EBPF_PASS; } else { // 条目存在 - 解包状态 window_start = UNPACK_TIMESTAMP(data); counter = UNPACK_COUNTER(data); // 检查我们是否仍在同一个时间窗口内 if (now_secs - window_start >= WINDOW_SECONDS) { // 窗口已过期 - 重置计数器并开始新窗口 window_start = now_secs; counter = 1; } else { // 仍在同一窗口内 - 递增计数器 counter++; // 检查是否超过速率限制 if (counter > RATE_LIMIT) { // 丢弃数据包而不更新状态 return CF_EBPF_DROP; } } } -
存储更新后的状态并放行数据包。
将窗口开始时间戳和计数器打包回单个值,并将其存储在源 IP 状态表中。
// 存储更新后的状态 uint64_t new_data = PACK_STATE(window_start, counter); set_src_ip_data(new_data); return CF_EBPF_PASS; }
作为参考,下面是完整的速率限制程序:
#include <cf_ebpf_defs.h>
#define CF_EBPF_HELPER_V0
#include <cf_ebpf_helper.h>
// 速率限制配置
// 此程序实现固定(非滑动)窗口速率限制。
#define RATE_LIMIT 100 // 每个窗口允许的最大数据包数
#define WINDOW_SECONDS 60 // 时间窗口秒数
// 源 IP 表持有从源 IP -> 自定义 u64 的映射。我们将让表中自定义的 u64 值
// 持有时间戳和计数器,以实现速率限制。
//
// 注意:源 IP 表实际上是一个 LRU 缓存。如果满了,旧值将被逐出。
// 表中的值每 1 小时也会进行一次垃圾回收。
//
// 下面的宏将时间戳(高 32 位)和计数器(低 32 位)打包到 64 位数据中,
// 以便我们可以将其存入源 IP 表。
#define PACK_STATE(ts, count) (((uint64_t)(ts) << 32) | ((uint64_t)(count) & 0xFFFFFFFF))
#define UNPACK_TIMESTAMP(data) ((uint32_t)((data) >> 32))
#define UNPACK_COUNTER(data) ((uint32_t)((data) & 0xFFFFFFFF))
uint64_t cf_ebpf_main(void *state)
{
// 获取当前时间戳
int64_t now = timestamp();
if (now < 0) {
return CF_EBPF_PASS; // 如果获取时间戳失败,则放行数据包
}
uint32_t now_secs = (uint32_t)now;
// 尝试获取该源 IP 的现有状态
uint64_t data;
int ret = get_src_ip_data(&data);
uint32_t window_start;
uint32_t counter;
if (ret == -1) {
// 无现有条目 - 首次来自该 IP 的数据包
// 初始化:窗口从现在开始,计数器 = 1
window_start = now_secs;
counter = 1;
} else if (ret != 0) {
// 如果获取 src_ip_data 出现其他未知错误,放行数据包
return CF_EBPF_PASS;
} else {
// 条目存在 - 解包状态
window_start = UNPACK_TIMESTAMP(data);
counter = UNPACK_COUNTER(data);
// 检查我们是否仍在同一个时间窗口内
if (now_secs - window_start >= WINDOW_SECONDS) {
// 窗口已过期 - 重置计数器并开始新窗口
window_start = now_secs;
counter = 1;
} else {
// 仍在同一窗口内 - 递增计数器
counter++;
// 检查是否超过速率限制
if (counter > RATE_LIMIT) {
// 丢弃数据包而不更新状态
// 此处为发生实际限流的位置。
return CF_EBPF_DROP;
}
}
}
// 存储更新后的状态
uint64_t new_data = PACK_STATE(window_start, counter);
set_src_ip_data(new_data);
return CF_EBPF_PASS;
}该程序展示了几个核心概念:
- 位打包 (Bit packing):使用位移将多个值(时间戳和计数器)存储在单个
u64中。 - 固定窗口速率限制:在离散的时间窗口内跟踪数据包计数,并在窗口到期时进行重置。
- 容错处理:在 helper 函数失败时放行数据包,以避免在边缘情况发生时导致合法流量被误杀。
- 状态表行为:源 IP 状态表是一个 LRU 缓存。如果达到容量限制,旧条目将被逐出。在闲置一小时后,条目也会被垃圾回收。
每个程序都可以访问其本地状态。状态对每台服务器都是本地的,不能在数据中心之间共享。
状态与特定程序绑定。如果修改规则模式(禁用、监控或启用),状态表的内容将保留。但是,如果修改规则的程序,或修改程序本身的内容,状态表将被清除。
有两个状态表可用于您的程序。
源 IP 状态表存储以源 IP 地址为键的状态。每个条目包含:
| 字段 | 类型 | 描述 |
|---|---|---|
| Status | Enum (枚举) | 源 IP 的状态:None (0), Challenged (1), Verified (2), 或 Blocklisted (3)。 |
| User data | u64 |
您可以出于任何目的设置的自定义用户定义值。 |
默认最大容量为 1000 个条目。
使用以下 helper 函数与该表进行交互:
get_src_ip_status— 检索当前数据包源 IP 的状态。set_src_ip_status— 设置当前数据包源 IP 的状态。get_src_ip_data— 检索当前数据包源 IP 的用户数据。set_src_ip_data— 存储当前数据包源 IP 的用户数据。
在以下情况下,将在源 IP 状态表中创建一条条目:
- 程序调用
set_src_ip_status将源 IP 标记为 Challenged, Verified, 或 Blocklisted。 - 程序调用
set_src_ip_data存储源 IP 的自定义 u64 数据。 - 程序为在表中没有现有条目的新源 IP 调用
set_challenge。
流状态表存储以四元组为键的状态:源 IP、源端口、目标 IP 和目标端口。每个条目包含一个您可以出于任何目的设置的 u64 值。
默认最大容量为 10,000 个条目。
使用以下 helper 函数与该表进行交互:
get_flow_data— 检索当前流的用户数据。set_flow_data— 存储当前流的用户数据。
在以下情况下,将在流状态表中创建一条条目:
- 程序调用
set_flow_data存储流的自定义 u64 数据。
两个状态表都是 LRU(最近最少使用)缓存。如果表格达到其最大容量,则最旧的条目将被逐出,以腾出空间存放新条目。如果一个小时内没有访问过条目,条目也会被垃圾回收。
helper 函数是 Cloudflare 运行时提供给客户程序调用的函数。
helper 函数至关重要,因为 BPF 指令集架构 (ISA) 仅支持某些系统调用。出于安全性考量,Cloudflare 仅使用预定义的已知库列表来编译 BPF 对象文件,程序开发人员无法修改这些库。
Helper 函数定义和校验器封装源码可以在 GitHub ↗ 上获取。
从 cf_ebpf_generic_ctx 和 cf_ebpf_packet_data 构造 cf_ebpf_parsed_headers。执行所需的内存检查以通过校验器。
static inline int parse_packet_data(
struct cf_ebpf_generic_ctx *ctx,
struct cf_ebpf_packet_data **out_p,
struct cf_ebpf_parsed_headers *out_headers
);参数:
ctx— 指向传递进 BPF 程序的通用上下文的指针。out_p— 指向接收数据包数据结构的指针。out_headers— 指向接收解析后的请求头结构的指针。
返回: 成功时返回 0,失败时返回 1(例如,数据包太短或长度无效)。成功时,out_headers 包含有效的 IP 和 UDP 请求头指针。
生成一个随机无符号整数。
uint64_t rand(void);返回: 一个随机的 uint64_t 值。
返回当前的 UNIX 时间戳(自 UTC 时间 1970 年 1 月 1 日 0:00:00 以来经过的非闰秒秒数)。
int64_t timestamp(void);返回: 以 int64_t 形式返回当前时间戳。
计算源缓冲区的 MD5 哈希,并将结果存储在目标缓冲区中。
int hash_md5(uint8_t *src, size_t src_len, uint8_t *dest, size_t dest_len);参数:
src— 指向源缓冲区的指针。src_len— 源缓冲区的字节长度。dest— 指向目标缓冲区的指针(必须至少为 16 字节)。dest_len— 目标缓冲区的字节长度。
返回:
- 成功时返回正值(写入的字节数)。
- 如果源缓冲区无效,则返回
-1。 - 如果目标缓冲区为 null 或太小,则返回
-2。
计算源缓冲区的 SHA-256 哈希,并将结果存储在目标缓冲区中。
int hash_sha256(uint8_t *src, size_t src_len, uint8_t *dest, size_t dest_len);参数:
src— 指向源缓冲区的指针。src_len— 源缓冲区的字节长度。dest— 指向目标缓冲区的指针(必须至少为 32 字节)。dest_len— 目标缓冲区的字节长度。
返回:
- 成功时返回正值(写入的字节数)。
- 如果源缓冲区无效,则返回
-1。 - 如果目标缓冲区为 null 或太小,则返回
-2。
计算源缓冲区的 SHA-512 哈希,并将结果存储在目标缓冲区中。
int hash_sha512(uint8_t *src, size_t src_len, uint8_t *dest, size_t dest_len);参数:
src— 指向源缓冲区的指针。src_len— 源缓冲区的字节长度。dest— 指向目标缓冲区的指针(必须至少为 64 字节)。dest_len— 目标缓冲区的字节长度。
返回:
- 成功时返回正值(写入的字节数)。
- 如果源缓冲区无效,则返回
-1。 - 如果目标缓冲区为 null 或太小,则返回
-2。
计算源缓冲区的 CRC32 哈希,并将结果作为 64 位整数存储。这是一个便利封装,会在内部处理字节到整数的转换。
int hash_crc32(uint8_t *src, size_t src_len, uint64_t *dest);参数:
src— 指向源缓冲区的指针。src_len— 源缓冲区的字节长度。dest— 指向接收 CRC32 结果的uint64_t的指针。
返回:
- 成功时返回正值(内部写入的字节数,始终为 8)。
- 如果源缓冲区无效,则返回
-1。 - 如果目标缓冲区为 null,则返回
-2。
计算源缓冲区的 BLAKE2B-512 哈希,并将结果存储在目标缓冲区中。
int hash_blake2b512(const uint8_t *src, size_t src_len, uint8_t *dest, size_t dest_len);参数:
src— 指向源缓冲区的指针。src_len— 源缓冲区的字节长度。dest— 指向目标缓冲区的指针(必须至少为 64 字节)。dest_len— 目标缓冲区的字节长度。
返回:
- 成功时返回正值(写入的字节数)。
- 如果源缓冲区无效,则返回
-1。 - 如果目标缓冲区为 null 或太小,则返回
-2。
计算源缓冲区的 HMAC-SHA256,并将结果存储在目标缓冲区中。私钥在平台级别配置,不会直接暴露给 BPF 程序。该密钥对每个服务器和每个客户都是唯一的。
int hmac_sha256(uint8_t *src, size_t src_len, uint8_t *dest, size_t dest_len);参数:
src— 指向源缓冲区的指针。src_len— 源缓冲区的字节长度。dest— 指向目标缓冲区的指针(必须至少为 32 字节)。dest_len— 目标缓冲区的字节长度。
返回:
- 成功时返回正值(写入的字节数)。
- 如果源缓冲区无效,则返回
-1。 - 如果目标缓冲区为 null 或太小,则返回
-2。
计算源缓冲区的 HMAC-SHA512,并将结果存储在目标缓冲区中。私钥在平台级别配置,不会直接暴露给 BPF 程序。该密钥对每个服务器和每个客户都是唯一的。
int hmac_sha512(uint8_t *src, size_t src_len, uint8_t *dest, size_t dest_len);参数:
src— 指向源缓冲区的指针.src_len— 源缓冲区的字节长度。dest— 指向目标缓冲区的指针(必须至少为 64 字节)。dest_len— 目标缓冲区的字节长度。
返回:
- 成功时返回正值(写入的字节数)。
- 如果源缓冲区无效,则返回
-1。 - 如果目标缓冲区为 null 或太小,则返回
-2。
计算源缓冲区的 BLAKE2B-512 HMAC,并将结果存储在目标缓冲区中。私钥在平台级别配置,不会直接暴露给 BPF 程序。该密钥对每个服务器和每个客户都是唯一的。
int hmac_blake2b512(const uint8_t *src, size_t src_len, uint8_t *dest, size_t dest_len);参数:
src— 指向源缓冲区的指针。src_len— 源缓冲区的字节长度。dest— 指向目标缓冲区的指针(必须至少为 64 字节)。dest_len— 目标缓冲区的字节长度。
返回:
- 成功时返回正值(写入的字节数)。
- 如果源缓冲区无效,则返回
-1。 - 如果目标缓冲区为 null 或太小,则返回
-2。
为当前数据包设置质询数据。使用此功能向客户端发回质询数据包。
int set_challenge(uint8_t *src, size_t src_len);参数:
src— 指向质询数据缓冲区的指针。src_len— 质询数据的字节长度。如果为0,则重置质询缓冲区。
返回:
- 成功时返回
0。 - 如果源缓冲区无效或超过最大允许大小,则返回
-4。 - 如果未启用质询,则返回
-5。 - 如果最近向该源 IP 发送过质询,或者超出了全局速率限制,则返回
-6。
从状态表中检索与源 IP 地址关联的状态值。
int get_src_ip_status(uint8_t *status, uint64_t *expiry);参数:
status— 指向接收状态值(CF_EBPF_SRC_IP_STATUS_CHALLENGED、CF_EBPF_SRC_IP_STATUS_VERIFIED或CF_EBPF_SRC_IP_STATUS_BLOCKLISTED)的指针。如果仅需要过期时间,可以为 null。expiry— 指向接收过期时间戳的指针。如果仅需要状态,可以为 null。
返回:
- 成功时返回
0。 - 如果源 IP 没有对应的条目,则返回
-1。 - 如果当前数据包没有设置源 IP 上下文,则返回
-2。 - 如果提供的缓冲区太小,则返回
-3。 - 如果
status和expiry都为 null,则返回-4。 - 如果未启用源 IP 状态表,则返回
-5。
在状态表中设置与源 IP 地址关联的状态值。
int set_src_ip_status(uint8_t status, uint64_t expiry_secs);参数:
status— 要设置的状态值(CF_EBPF_SRC_IP_STATUS_CHALLENGED、CF_EBPF_SRC_IP_STATUS_VERIFIED或CF_EBPF_SRC_IP_STATUS_BLOCKLISTED)。expiry_secs— 状态过期前的秒数。如果为0,则状态永不过期。
返回:
- 成功时返回
0。 - 如果当前数据包没有设置源 IP 上下文,则返回
-2。 - 如果未启用源 IP 状态表,则返回
-5。
从状态表中检索与源 IP 地址关联的自定义数据。
int get_src_ip_data(uint64_t *data);参数:
data— 指向接收存储的数据值的指针。
返回:
- 成功时返回
0。 - 如果源 IP 没有对应的条目,则返回
-1。 - 如果当前数据包没有设置源 IP 上下文,则返回
-2。 - 如果提供的缓冲区太小,则返回
-3。 - 如果
data为 null,则返回-4。 - 如果未启用源 IP 状态表,则返回
-5。
在状态表中存储与源 IP 地址关联的自定义数据。
int set_src_ip_data(uint64_t data);参数:
data— 要存储的数据值。
返回:
- 成功时返回
0。 - 如果当前数据包没有设置源 IP 上下文,则返回
-2。 - 如果未启用源 IP 状态表,则返回
-5。
从状态表中检索与当前流关联的自定义数据。
int get_flow_data(uint64_t *data);参数:
data— 指向接收存储的数据值的指针。
返回:
- 成功时返回
0。 - 如果流没有对应的条目,则返回
-1。 - 如果当前数据包没有设置流上下文,则返回
-2。 - 如果提供的缓冲区太小,则返回
-3。 - 如果
data为 null 或未对齐,则返回-4。 - 如果未启用流状态表,则返回
-5。
在状态表中存储与当前流关联的自定义数据。
int set_flow_data(uint64_t data);参数:
data— 要存储的数据值。
返回:
- 成功时返回
0。 - 如果当前数据包没有设置流上下文,则返回
-2。 - 如果未启用流状态表,则返回
-5。
计算源缓冲区的香农熵 (Shannon entropy)。结果以千分之一比特 (millibits) 为单位返回,范围从 0(所有字节完全相同)到 8000(所有 256 个字节值均分)。
int64_t entropy(uint8_t *src, size_t src_len);参数:
src— 指向源缓冲区的指针。src_len— 源缓冲区的字节长度。
返回:
- 成功时返回以毫比特为单位的熵值 (0-8000)。
- 如果源缓冲区无效,则返回
-1。
为网络分析报告设置自定义标记。该标记与数据包样本一起显示在 Network Analytics 仪表板中。默认情况下,数据包以 1/10,000 的速率进行采样。
每次程序执行仅设置一个标记。如果程序执行多次调用 set_network_analytics_tag,则最后的标记值将应用于该数据包样本。
int set_network_analytics_tag(uint64_t tag);参数:
tag— 要设置的标记值。如果未设置,则默认为0。
返回: 成功时返回 0。
将一个 16 位整数从网络字节顺序转换为网络主机字节顺序。
uint16_t ntohs(uint16_t netshort);参数:
netshort— 网络字节顺序下的 16 位数值。
返回: 主机字节顺序下的数值。
将一个 16 位整数从主机字节顺序转换为网络字节顺序。
uint16_t htons(uint16_t hostshort);参数:
hostshort— 主机字节顺序下的 16 位数值。
返回: 网络字节顺序下的数值。
将一个 32 位整数从网络字节顺序转换为网络主机字节顺序。
uint32_t ntohl(uint32_t netlong);参数:
netlong— 网络字节顺序下的 32 位数值。
返回: 主机字节顺序下的数值。
将一个 32 位整数从主机字节顺序转换为网络字节顺序。
uint32_t htonl(uint32_t hostlong);参数:
hostlong— 主机字节顺序下的 32 位数值。
返回: 网络字节顺序下的数值。
将一个 64 位整数从网络字节顺序转换为网络主机字节顺序。
uint64_t ntohll(uint64_t netlonglong);参数:
netlonglong— 网络字节顺序下的 64 位数值。
返回: 主机字节顺序下的数值。
将一个 64 位整数从主机字节顺序转换为网络字节顺序。
uint64_t htonll(uint64_t hostlonglong);参数:
hostlonglong— 主机字节顺序下的 64 位数值。
返回: 网络字节顺序下的数值。
传递进 BPF 程序的通用上下文结构体。
struct cf_ebpf_generic_ctx {
/* 指向上下文数据开始位置的指针。 */
uint64_t data;
/* 指向上下文数据结束位置的指针。 */
uint64_t data_end;
/* 供程序存储元数据的空间。 */
uint64_t meta_data;
};包含传递给 BPF 程序的原始数据包数据。
struct cf_ebpf_packet_data {
/* 数据包的总长度。 */
size_t total_packet_length;
/* IP 请求头的大小。支持 IPv4(包括选项)和 IPv6。 */
size_t ip_header_length;
/* 数据包的字节,从 IP 请求头开始。 */
uint8_t packet_buffer[1500];
};包含指向解析后的 IP 和 UDP 请求头的指针。通过调用 parse_packet_data 填充。
struct cf_ebpf_parsed_headers {
/* 指向解析后的 IPv4 请求头的指针,如果存在(否则为 null)。 */
struct iphdr *ipv4;
/* 指向解析后的 IPv6 请求头的指针,如果存在(否则为 null)。 */
struct ipv6hdr *ipv6;
/* 指向解析后的 UDP 请求头的指针。 */
struct udphdr *udp;
/* 指向数据包上下文数据最后一个有效字节的原始指针。 */
uint8_t *data_end;
};IPv4 请求头结构。来源:Linux kernel ↗。
struct iphdr {
#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
uint8_t version:4,
ihl:4;
#else
uint8_t ihl:4,
version:4;
#endif
uint8_t tos;
uint16_t tot_len;
uint16_t id;
uint16_t frag_off;
uint8_t ttl;
uint8_t protocol;
uint16_t check;
uint32_t saddr;
uint32_t daddr;
};IPv6 请求头结构。来源:Linux kernel ↗。
struct ipv6hdr {
#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
uint8_t version:4,
priority:4;
#else
uint8_t priority:4,
version:4;
#endif
uint8_t flow_lbl[3];
uint16_t payload_len;
uint8_t nexthdr;
uint8_t hop_limit;
uint8_t saddr[16];
uint8_t daddr[16];
};UDP 请求头结构。来源:Linux kernel ↗。
struct udphdr {
uint16_t source;
uint16_t dest;
uint16_t len;
uint16_t check;
};要上传程序,请在 Cloudflare 仪表板中导航到 Networking(网络) > L3/4 DDoS protection(L3/4 DDoS 防护) > Advanced Protection(高级防护)。然后选择名为 Programmable Flow Protection(可编程流防护) 的选项卡。
在 Programs(程序) 下,点击按钮 Upload new program(上传新程序)。这将提示您选择一个要上传的包含您 C 源代码的文件。
Cloudflare API 将接收 C 文件中的源代码,将其编译为 BPF 字节码,并对其运行校验器。
如果编译或校验失败,API 将返回详细的错误消息。
如果编译和验证成功,Cloudflare 将源代码和对象文件存储到该账户,并返回程序 ID。
在开发过程中,您可能会发现更新同一个程序(由同一个程序 ID 标识)比重复创建新程序作为新资源更为有用。
要更新程序,请选择您的程序旁边的三个点。然后,选择 Overwrite(覆写)。这将提示您选择一个文件作为 C 源代码上传。
要查看所有上传的程序及其成功状态,请查看 Programs(程序) 部分下的表格。
程序名称旁边的链接图标表示该程序目前正在活跃规则中使用,不得删除。
要删除程序,请选择您要删除的程序旁边的三个点。然后,选择 Delete(删除)。
注意,您无法删除在活跃规则中引用的程序。
注意,处于 failed 状态(即编译或验证失败)的程序在闲置 30 天后将被自动永久删除。
每个数据包仅执行一条规则。如果您的账户配置了多条规则,则执行具有最具体范围的规则。例如,作用于特定数据中心 (colo) 的规则优先级高于作用于区域的规则,后者优先级高于全局规则。这就是为什么您不能创建多条全局规则的原因。
要查看规则及其关联的规则 ID,请在 Cloudflare 仪表板中前往 Networking(网络) > L3/4 DDoS protection(L3/4 DDoS 防护) > Advanced Protection(高级防护)。然后选择 Programmable Flow Protection(可编程流防护)。
要创建规则,请在 Cloudflare 仪表板中前往 Networking(网络) > L3/4 DDoS protection(L3/4 DDoS 防护) > Advanced Protection(高级防护)。然后选择 Programmable Flow Protection(可编程流防护)。
在 Rules(规则) 下,选择 Create rule(创建规则)。填写新规则的相应字段。系统将提示您为规则选择程序、模式和范围。
要更新现有规则,请导航至 Rules(规则) 部分。点击规则旁边的三个点并选择 Edit(编辑)。
系统将提示您编辑规则的模式和范围。您不可以编辑规则的程序,因为这是一项不安全的滚动更新方式。
要删除现有规则,请导航至 Rules(规则) 部分。点击规则旁边的三个点并选择 Delete(删除)。
此 API 端点通过读入以下内容来调试程序:
- 提供的作为请求数据的二进制格式的本地 PCAP 文件路径。输入 PCAP 文件的最大大小限制为 5 MB,如果太大将被拒绝。
- 请求路径中提供的程序 ID。
- 一个可选的查询参数
ip_offset=<value>以指定 IP 偏移量。这是输入 PCAP 文件的每个数据包中 IP 请求头所偏移的字节数。 如果省略了ip_offset查询参数,API 将对正确的偏移值进行合理推测。 例如,如果 PCAP 文件捕获的是以太网数据包,则检测到的 IP 偏移值将为 14。此端点假定 PCAP 中的所有数据包都具有相同的 IP 偏移值,否则将错误地解析数据包。
该端点针对输入 PCAP 运行引用的 BPF 程序,并输出一个新的带注释的 PCAP 文件。输出的 PCAP 文件将包含与输入 PCAP 文件完全相同的数据包,并且还将在每个数据包的 Packet Comment(数据包注释) 部分中包含标注的程序裁定。
curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/magic/programmable_flow_protection/configs/programs/$PROGRAM_ID/pcap" \
--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
--header "Content-Type: application/vnd.tcpdump.pcap" \
--data-binary "@<PATH_TO_INPUT_PCAP_FILE>" \
--output output.pcap数据包注释 (Packet Comment) 注释可能包含:
- 程序返回值:
CF_EBPF_PASS或CF_EBPF_DROP Ignored:如果传入的数据包不是 UDPAnalytics tag:程序在此数据包上设置的自定义网络分析标记(如有)
输出的 PCAP 文件还可能包含:
Challenge packet:从程序发射回客户端的质询数据包(如有)
您会希望在不影响现有生产流量的情况下,安全地部署和测试程序。初始部署方法可以是:将一个全局范围的规则设置为 disabled,并将一个数据中心或区域级别的规则设置为 monitoring,其过滤器表达式仅作用于 IP 流量的某个子集。
每个 Cloudflare 区域或数据中心都将应用最精细的规则。因此,在上述场景中,在 monitoring 规则中指定的数据中心或区域将以 monitoring 模式执行开发人员程序,而其他所有 Cloudflare 位置将根本不执行该程序。monitoring 规则将仅在与过滤器表达式匹配的流量上执行。
然后,在通过 Network Analytics 验证了正确的行为之后,您可以更新并扩大 monitoring 规则的范围和过滤器表达式。最终,您可以删除 disabled 和 monitoring 规则,并应用全局的 enabled 规则。
使用 Expression 字段将程序限制在 IP 或前缀的子集内,并使用 Mode 字段来指示程序是否实际丢弃数据包,以确保在上线时程序的安全性和精细度。
流经可编程流防护的流量可以在 Network Analytics (网络分析) 仪表板中找到。
在 Network Analytics 仪表板中,选择 Programmable Flow Protection(可编程流防护) 选项卡以基于此功能筛选流量。您可以按程序 ID、自定义网络分析标记、操作、IP 和端口过滤流量。默认情况下,数据包的采样率为 1/10,000。