跳转到内容
搜索文档

可编程流防护 (Beta)

最后更新 查看 MarkdownAgent 设置

可编程流防护 (Programmable Flow Protection) 是一种 DDoS 防护系统,可针对基于自定义或标准第 7 层 UDP 的协议(例如游戏协议、金融服务协议、VoIP、电信和流媒体)防范 DDoS 攻击。在拓扑结构方面,它同时支持非对称和对称配置,但仅审查入口 (ingress) 流量。

可编程流防护目前处于非公开测试 (closed beta) 阶段,仅作为 Magic TransitBYOIP 或 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(可编程流防护) 选项卡内:

  1. 上传您用 C 编写的 eBPF 程序。

    系统会校验该程序并将其存储在您的账户中。API 会编译该程序,然后针对编译后的程序运行校验器,以强制执行内存检查并验证程序能够终止。如果程序编译或校验失败,Cloudflare 仪表板将返回详细的错误消息。

  2. 创建规则

  3. 要观察程序的行为,请检查 Network Analytics 仪表板并选择 Programmable Flow Protection(可编程流防护) 选项卡。

您可以创建具有不同规则设置的其他规则,这些规则可以范围限定到各种区域和 Cloudflare 位置,以更改其模式(缓解或监控),从而适应您的流量模式和业务用例。

可编程流防护系统支持 Data Localization 套件

编写基础程序

以下步骤演示了如何编写一个示例程序,该程序丢弃所有带有 IPv6 请求头的用户数据报协议 (UDP) 流量。它还会丢弃发送到端口 66 的流量,以及在 UDP 负载中不包含特定自定义应用请求头值的流量。

  1. 添加一个 define 指令来指定所使用的 helper 函数版本。

    随着 Cloudflare 向可编程流防护 API 添加更多功能,我们将发布新版本的 API。各版本保证向后兼容。

    #define CF_EBPF_HELPER_V0
  2. 引入 Cloudflare eBPF 头文件。

    这些文件包含 helper 函数,用于将输入的包裹数据解析并传递给 BPF 程序。

    #include <cf_ebpf_defs.h>
    #include <cf_ebpf_helper.h>
  3. 定义用于数据包处理的入口函数。

    您的程序必须具有以下确切的函数签名,才能妥善通过 Cloudflare 的程序校验。

    返回类型 uint64_t 决定了 Cloudflare 是放行还是丢弃数据包。函数名 cf_ebpf_main 用作程序的入口点。参数 void *state 是指 Cloudflare 提供给您的 BPF 程序的输入数据。

    uint64_t cf_ebpf_main(void *state)
  4. 将输入参数转换为可用的结构体。

    将输入数据转换为 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;
  5. 通过调用 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 函数和结构体

  6. 编写您的自定义逻辑。

    先前的步骤已经确立了对您编写的任何程序来说都是相同的代码,无论其逻辑如何。

    现在,您可以编写您自己的自定义逻辑了。

    在下方的示例片段中,程序将丢弃存在 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;
     }
  7. 对于没有被程序逻辑丢弃的任何数据包,通过返回 CF_EBPF_PASS 来放行。

    目前支持的返回值是:

    • CF_EBPF_PASS = 返回值 0
    • CF_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 的数据包将不经进一步检查直接通过。

  1. 包含 Cloudflare eBPF 头文件并定义 helper 版本。

    #define CF_EBPF_HELPER_V0
    
    #include <cf_ebpf_defs.h>
    #include <cf_ebpf_helper.h>
  2. 定义质询响应协议的常量。

    质询响应是通过将 nonce 与一个 secret 密钥值进行 XOR 计算出来的。过期时间决定了被质询或已验证状态保持有效的时长。

    #define CHALLENGE_SECRET 0xDEADBEEFCAFEBABEULL
    #define CHALLENGE_EXPIRY_SECS 60
    #define VERIFIED_EXPIRY_SECS 3600
  3. 定义质询数据包的结构体。

    质询数据包包含客户端必须对其做出响应的 nonce,以及用于客户端响应的空间。

    struct challenge_packet {
        uint64_t nonce;        // 用于此质询的随机 nonce
        uint64_t response;     // 预期值:nonce XOR CHALLENGE_SECRET
    };
  4. 定义入口函数并解析数据包。

    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;
  5. 使用 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;
        }
  6. 处理已校验的源 IP。

    可编程流防护平台将在调用程序之前,丢弃来自黑名单 IP 的数据包。无需在程序中显式处理黑名单情况。

    如果源 IP 已被验证(通过了之前的质询),则放行该数据包。

        if (ret == 0 && status == CF_EBPF_SRC_IP_STATUS_VERIFIED) {
            return CF_EBPF_PASS;
        }
  7. 检查这是否是来自已被质询源 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;
        }
  8. 针对新的源 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_statusset_src_ip_statusget_src_ip_dataset_src_ip_data 来追踪每个源 IP 的质询状态。
  • 质询发射:使用 set_challenge 发送回客户端一个质询数据包。
  • 密码学验证:使用共享密钥验证客户端是否正确响应了质询。
  • 过期处理:使用时间戳来让陈旧的状态条目过期。

编写复杂程序:速率限制

下面的示例程序实现了一个使用固定窗口算法的每个源 IP 的速率限制器。通过限制单个源 IP 在一个时间窗口内可以发送的数据包数量,这对于缓解体积型 DDoS 攻击非常有用。

速率限制机制工作原理如下:

当一个数据包到达时,程序会检索为该源 IP 存储的状态。该状态包含了一个窗口开始时间戳和一个数据包计数器,打包在单个 64 位值中。如果当前时间仍在窗口内,计数器会递增。如果计数器超过了配置的限制,数据包将被丢弃。当窗口过期时,计数器会重置。

  1. 包含 Cloudflare eBPF 头文件并定义 helper 版本。

    #include <cf_ebpf_defs.h>
    #define CF_EBPF_HELPER_V0
    #include <cf_ebpf_helper.h>
  2. 定义速率限制配置的常量。

    RATE_LIMIT 设置每个窗口允许的最大数据包数量。WINDOW_SECONDS 定义每个时间窗口的持续秒数。

    #define RATE_LIMIT 100         // 每个窗口允许的最大数据包数
    #define WINDOW_SECONDS 60      // 时间窗口秒数
  3. 定义打包和解包状态数据的宏。

    源 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))
  4. 定义入口函数并获取当前时间戳。

    如果获取时间戳的 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;
  5. 检索该源 IP 的现有状态。

    使用 get_src_ip_data 来查找此源 IP 之前是否被记录过。

        // 尝试获取该源 IP 的现有状态
        uint64_t data;
        int ret = get_src_ip_data(&data);
        uint32_t window_start;
        uint32_t counter;
  6. 处理这是个新源 IP 的情况。

    如果没有现有的条目(返回值为 -1),这是来自该源 IP 的第一个数据包。初始化窗口自现在开始,计数器为 1。

        if (ret == -1) {
            // 无现有条目 - 首次来自该 IP 的数据包
            // 初始化:窗口从现在开始,计数器 = 1
            window_start = now_secs;
            counter = 1;
        }
  7. 处理现有的源 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;
                }
            }
        }
  8. 存储更新后的状态并放行数据包。

    将窗口开始时间戳和计数器打包回单个值,并将其存储在源 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 缓存。如果达到容量限制,旧条目将被逐出。在闲置一小时后,条目也会被垃圾回收。

状态 (State)

每个程序都可以访问其本地状态。状态对每台服务器都是本地的,不能在数据中心之间共享。

状态与特定程序绑定。如果修改规则模式(禁用、监控或启用),状态表的内容将保留。但是,如果修改规则的程序,或修改程序本身的内容,状态表将被清除。

有两个状态表可用于您的程序。

源 IP 状态表

源 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 状态表中创建一条条目:

  1. 程序调用 set_src_ip_status 将源 IP 标记为 Challenged, Verified, 或 Blocklisted。
  2. 程序调用 set_src_ip_data 存储源 IP 的自定义 u64 数据。
  3. 程序为在表中没有现有条目的新源 IP 调用 set_challenge

流状态表 (Flow state table)

流状态表存储以四元组为键的状态:源 IP、源端口、目标 IP 和目标端口。每个条目包含一个您可以出于任何目的设置的 u64 值。

默认最大容量为 10,000 个条目。

使用以下 helper 函数与该表进行交互:

  • get_flow_data — 检索当前流的用户数据。
  • set_flow_data — 存储当前流的用户数据。

在以下情况下,将在流状态表中创建一条条目:

  1. 程序调用 set_flow_data 存储流的自定义 u64 数据。

缓存行为

两个状态表都是 LRU(最近最少使用)缓存。如果表格达到其最大容量,则最旧的条目将被逐出,以腾出空间存放新条目。如果一个小时内没有访问过条目,条目也会被垃圾回收。


BPF helper 函数和结构体

helper 函数是 Cloudflare 运行时提供给客户程序调用的函数。

helper 函数至关重要,因为 BPF 指令集架构 (ISA) 仅支持某些系统调用。出于安全性考量,Cloudflare 仅使用预定义的已知库列表来编译 BPF 对象文件,程序开发人员无法修改这些库。

Helper 函数定义和校验器封装源码可以在 GitHub 上获取。

Helper 函数

parse_packet_data

cf_ebpf_generic_ctxcf_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 请求头指针。

rand

生成一个随机无符号整数。

uint64_t rand(void);

返回: 一个随机的 uint64_t 值。

timestamp

返回当前的 UNIX 时间戳(自 UTC 时间 1970 年 1 月 1 日 0:00:00 以来经过的非闰秒秒数)。

int64_t timestamp(void);

返回:int64_t 形式返回当前时间戳。

hash_md5

计算源缓冲区的 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

hash_sha256

计算源缓冲区的 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

hash_sha512

计算源缓冲区的 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

hash_crc32

计算源缓冲区的 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

hash_blake2b512

计算源缓冲区的 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

计算源缓冲区的 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

计算源缓冲区的 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

hmac_blake2b512

计算源缓冲区的 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

set_challenge

为当前数据包设置质询数据。使用此功能向客户端发回质询数据包。

int set_challenge(uint8_t *src, size_t src_len);

参数:

  • src — 指向质询数据缓冲区的指针。
  • src_len — 质询数据的字节长度。如果为 0,则重置质询缓冲区。

返回:

  • 成功时返回 0
  • 如果源缓冲区无效或超过最大允许大小,则返回 -4
  • 如果未启用质询,则返回 -5
  • 如果最近向该源 IP 发送过质询,或者超出了全局速率限制,则返回 -6

get_src_ip_status

从状态表中检索与源 IP 地址关联的状态值。

int get_src_ip_status(uint8_t *status, uint64_t *expiry);

参数:

  • status — 指向接收状态值(CF_EBPF_SRC_IP_STATUS_CHALLENGEDCF_EBPF_SRC_IP_STATUS_VERIFIEDCF_EBPF_SRC_IP_STATUS_BLOCKLISTED)的指针。如果仅需要过期时间,可以为 null。
  • expiry — 指向接收过期时间戳的指针。如果仅需要状态,可以为 null。

返回:

  • 成功时返回 0
  • 如果源 IP 没有对应的条目,则返回 -1
  • 如果当前数据包没有设置源 IP 上下文,则返回 -2
  • 如果提供的缓冲区太小,则返回 -3
  • 如果 statusexpiry 都为 null,则返回 -4
  • 如果未启用源 IP 状态表,则返回 -5

set_src_ip_status

在状态表中设置与源 IP 地址关联的状态值。

int set_src_ip_status(uint8_t status, uint64_t expiry_secs);

参数:

  • status — 要设置的状态值(CF_EBPF_SRC_IP_STATUS_CHALLENGEDCF_EBPF_SRC_IP_STATUS_VERIFIEDCF_EBPF_SRC_IP_STATUS_BLOCKLISTED)。
  • expiry_secs — 状态过期前的秒数。如果为 0,则状态永不过期。

返回:

  • 成功时返回 0
  • 如果当前数据包没有设置源 IP 上下文,则返回 -2
  • 如果未启用源 IP 状态表,则返回 -5

get_src_ip_data

从状态表中检索与源 IP 地址关联的自定义数据。

int get_src_ip_data(uint64_t *data);

参数:

  • data — 指向接收存储的数据值的指针。

返回:

  • 成功时返回 0
  • 如果源 IP 没有对应的条目,则返回 -1
  • 如果当前数据包没有设置源 IP 上下文,则返回 -2
  • 如果提供的缓冲区太小,则返回 -3
  • 如果 data 为 null,则返回 -4
  • 如果未启用源 IP 状态表,则返回 -5

set_src_ip_data

在状态表中存储与源 IP 地址关联的自定义数据。

int set_src_ip_data(uint64_t data);

参数:

  • data — 要存储的数据值。

返回:

  • 成功时返回 0
  • 如果当前数据包没有设置源 IP 上下文,则返回 -2
  • 如果未启用源 IP 状态表,则返回 -5

get_flow_data

从状态表中检索与当前流关联的自定义数据。

int get_flow_data(uint64_t *data);

参数:

  • data — 指向接收存储的数据值的指针。

返回:

  • 成功时返回 0
  • 如果流没有对应的条目,则返回 -1
  • 如果当前数据包没有设置流上下文,则返回 -2
  • 如果提供的缓冲区太小,则返回 -3
  • 如果 data 为 null 或未对齐,则返回 -4
  • 如果未启用流状态表,则返回 -5

set_flow_data

在状态表中存储与当前流关联的自定义数据。

int set_flow_data(uint64_t data);

参数:

  • data — 要存储的数据值。

返回:

  • 成功时返回 0
  • 如果当前数据包没有设置流上下文,则返回 -2
  • 如果未启用流状态表,则返回 -5

entropy

计算源缓冲区的香农熵 (Shannon entropy)。结果以千分之一比特 (millibits) 为单位返回,范围从 0(所有字节完全相同)到 8000(所有 256 个字节值均分)。

int64_t entropy(uint8_t *src, size_t src_len);

参数:

  • src — 指向源缓冲区的指针。
  • src_len — 源缓冲区的字节长度。

返回:

  • 成功时返回以毫比特为单位的熵值 (0-8000)。
  • 如果源缓冲区无效,则返回 -1

set_network_analytics_tag

为网络分析报告设置自定义标记。该标记与数据包样本一起显示在 Network Analytics 仪表板中。默认情况下,数据包以 1/10,000 的速率进行采样。 每次程序执行仅设置一个标记。如果程序执行多次调用 set_network_analytics_tag,则最后的标记值将应用于该数据包样本。

int set_network_analytics_tag(uint64_t tag);

参数:

  • tag — 要设置的标记值。如果未设置,则默认为 0

返回: 成功时返回 0

ntohs

将一个 16 位整数从网络字节顺序转换为网络主机字节顺序。

uint16_t ntohs(uint16_t netshort);

参数:

  • netshort — 网络字节顺序下的 16 位数值。

返回: 主机字节顺序下的数值。

htons

将一个 16 位整数从主机字节顺序转换为网络字节顺序。

uint16_t htons(uint16_t hostshort);

参数:

  • hostshort — 主机字节顺序下的 16 位数值。

返回: 网络字节顺序下的数值。

ntohl

将一个 32 位整数从网络字节顺序转换为网络主机字节顺序。

uint32_t ntohl(uint32_t netlong);

参数:

  • netlong — 网络字节顺序下的 32 位数值。

返回: 主机字节顺序下的数值。

htonl

将一个 32 位整数从主机字节顺序转换为网络字节顺序。

uint32_t htonl(uint32_t hostlong);

参数:

  • hostlong — 主机字节顺序下的 32 位数值。

返回: 网络字节顺序下的数值。

ntohll

将一个 64 位整数从网络字节顺序转换为网络主机字节顺序。

uint64_t ntohll(uint64_t netlonglong);

参数:

  • netlonglong — 网络字节顺序下的 64 位数值。

返回: 主机字节顺序下的数值。

htonll

将一个 64 位整数从主机字节顺序转换为网络字节顺序。

uint64_t htonll(uint64_t hostlonglong);

参数:

  • hostlonglong — 主机字节顺序下的 64 位数值。

返回: 网络字节顺序下的数值。

结构体 (Structures)

cf_ebpf_generic_ctx

传递进 BPF 程序的通用上下文结构体。

struct cf_ebpf_generic_ctx {
   /* 指向上下文数据开始位置的指针。 */
   uint64_t data;
   /* 指向上下文数据结束位置的指针。 */
   uint64_t data_end;
   /* 供程序存储元数据的空间。 */
   uint64_t meta_data;
};

cf_ebpf_packet_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];
};

cf_ebpf_parsed_headers

包含指向解析后的 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;
};

iphdr

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;
};

ipv6hdr

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];
};

udphdr

UDP 请求头结构。来源:Linux kernel

struct udphdr {
    uint16_t source;
    uint16_t dest;
    uint16_t len;
    uint16_t check;
};

程序端点 (Program endpoints)

上传一个程序

要上传程序,请在 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 天后将被自动永久删除。


规则 (Rules)

每个数据包仅执行一条规则。如果您的账户配置了多条规则,则执行具有最具体范围的规则。例如,作用于特定数据中心 (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(删除)


调试数据包捕获 (PCAP)

此 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(数据包注释) 部分中包含标注的程序裁定。

请求bash
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_PASSCF_EBPF_DROP
  • Ignored:如果传入的数据包不是 UDP
  • Analytics tag:程序在此数据包上设置的自定义网络分析标记(如有)

输出的 PCAP 文件还可能包含:

  • Challenge packet:从程序发射回客户端的质询数据包(如有)

安全的程序和规则部署最佳实践

您会希望在不影响现有生产流量的情况下,安全地部署和测试程序。初始部署方法可以是:将一个全局范围的规则设置为 disabled,并将一个数据中心或区域级别的规则设置为 monitoring,其过滤器表达式仅作用于 IP 流量的某个子集。

每个 Cloudflare 区域或数据中心都将应用最精细的规则。因此,在上述场景中,在 monitoring 规则中指定的数据中心或区域将以 monitoring 模式执行开发人员程序,而其他所有 Cloudflare 位置将根本不执行该程序。monitoring 规则将仅在与过滤器表达式匹配的流量上执行。

然后,在通过 Network Analytics 验证了正确的行为之后,您可以更新并扩大 monitoring 规则的范围和过滤器表达式。最终,您可以删除 disabledmonitoring 规则,并应用全局的 enabled 规则。

使用 Expression 字段将程序限制在 IP 或前缀的子集内,并使用 Mode 字段来指示程序是否实际丢弃数据包,以确保在上线时程序的安全性和精细度。


Network Analytics (网络分析)

流经可编程流防护的流量可以在 Network Analytics (网络分析) 仪表板中找到。

在 Network Analytics 仪表板中,选择 Programmable Flow Protection(可编程流防护) 选项卡以基于此功能筛选流量。您可以按程序 ID、自定义网络分析标记、操作、IP 和端口过滤流量。默认情况下,数据包的采样率为 1/10,000。

这篇文档对您有帮助吗?