Programmable Flow Protection (Beta)
Programmable Flow Protection is a DDoS protection system that protects against DDoS attacks over custom or standardized Layer 7 UDP-based protocols, such as gaming protocols, financial services protocols, VoIP, telecom, and streaming. In terms of topology, it supports both asymmetric and symmetric configurations, but it will only inspect ingress traffic.
Programmable Flow Protection is currently in closed beta and available as an add-on for the Magic Transit (BYOIP or Cloudflare-leased IPs) service only. If you would like to enable the system, contact your account team or fill out this form ↗.
The Programmable Flow Protection system allows you to write and run your own packet-layer stateful program in C across Cloudflare's global anycast network as extended Berkeley Packet Filter (eBPF) programs running in the user space. An eBPF program ↗ is a packet filter system that allows a developer to write performant custom networking logic.
Programmable Flow Protection inspects and parses your UDP-based application's protocols (deep packet inspection) and determines the outcome of the packets based on your program. Using your custom program's logic, you can permit authorized users while actively blocking attacks.
The system is built on top of the flowtrackd platform, Cloudflare's stateful mitigation platform. The Programmable Flow Protection system relies on the DDoS Advanced Protection system's general settings to operate. It respects the prefixes that you have selected to route through the Advanced Protection systems, as well as the allowlist. The Advanced DDoS Protection system should be enabled for the Programmable Flow Protection system to operate.
While in beta, Cloudflare will assist and provide guidance to users to write their own code. Out-of-the-box code snippets (templates) for popular gaming protocols and VoIP protocols may be provided later on.
After Programmable Flow Protection has been enabled to your account, go to Networking > L3/4 DDoS Protection > Advanced Protection in the Cloudflare dashboard. Within the Programmable Flow Protection tab:
-
Upload your eBPF program written in C.
The program is validated by the system and stored in your account. The API compiles the program, then runs a verifier against the compiled program to enforce memory checks and verify program termination. If the program fails compilation or verification, the Cloudflare dashboard will return a detailed error message.
-
Create a rule
-
To observe the program's behavior, query the
programmableFlowProtectionNetworkAnalyticsAdaptiveGroupsgroup in GraphQL.
You can create additional rules with different rule settings scoped to various regions and Cloudflare locations to change the mode (Mitigation or Monitoring) to accommodate for your traffic patterns and business use cases.
The Programmable Flow Protection system supports the Data Localization suite.
The steps below write a sample program that drops all User Datagram Protocol (UDP) traffic with an IPv6 header. It also drops traffic destined to port 66, as well as traffic that does not have some custom specific application header value in the UDP payload.
-
Add a define directive to specify the versioned helper functions in use.
As Cloudflare adds more features to the Programmable Flow Protection API, we will publish new versions of its API. Versions are guaranteed to be backwards compatible.
#define CF_EBPF_HELPER_V0 -
Include the Cloudflare eBPF header files.
These files have helper functions to parse the input packet data to the BPF program.
#include <cf_ebpf_defs.h>#include <cf_ebpf_helper.h> -
Define the entry function for packet processing.
Your program must have the exact function signature below to properly pass Cloudflare's program verification.
The return type
uint64_tdictates whether Cloudflare will pass or drop a packet. The function namecf_ebpf_mainis used as the entrypoint to the program. The argumentvoid *staterefers to the data Cloudflare provides as input to your BPF program.uint64_t cf_ebpf_main(void *state) -
Cast the input argument into usable structs.
Convert the input data into
cf_ebpf_generic_ctx, which tells Cloudflare the data boundaries in the memory that we are reading.Then, declare variables for data parsing.
cf_ebpf_parsed_headerswill contain the IPv4, IPv6, and UDP headers.cf_ebpf_packet_datawill hold a copy of the original IP packet that Cloudflare received (maximum 1,500 bytes), as well as the packet length and IP header length.struct cf_ebpf_generic_ctx *ctx = state;struct cf_ebpf_parsed_headers headers;struct cf_ebpf_packet_data *p; -
Fill variables by calling the helper function.
You must fill in the variables by calling the helper function
parse_packet_data, which Cloudflare has provided in a header file included in step 2.The
parse_packet_datafunction performs the memory checks required to pass the program verifier. Theparse_packet_datafunction returns0on success. If it is successful, the input parameters are correctly populated. Theparse_packet_datafunction returns1on failure. Ifparse_packet_datafails, The program must returnCF_EBPF_DROPto drop the packet in order to pass the verifier.if (parse_packet_data(ctx, &p, &headers) != 0) {return CF_EBPF_DROP;}Available values after successful parsing:
struct cf_ebpf_packet_data {/* Total length of the packet. */size_t total_packet_length;/* Size of the IP header. Supports IPv4 (including options) and IPv6. */size_t ip_header_length;/* Bytes of the packet, starting with the IP header. */uint8_t packet_buffer[1500];};struct cf_ebpf_parsed_headers {/* Pointer to the parsed IPv4 header, if present (otherwise null). */struct iphdr *ipv4;/* Pointer to the parsed IPv6 header, if present (otherwise null). */struct ipv6hdr *ipv6;/* Pointer to the parsed UDP header. */struct udphdr *udp;/* Raw pointer to the last valid byte of the packet context data. */uint8_t *data_end;};For a full definition of helper functions and structures, refer to Supported BPF helper functions and structures.
-
Write your custom logic.
Prior steps have established the code that should be the same for any program that you write, regardless of its logic.
Now, you can write your own custom logic.
In the example snippet below, the program will drop any packet where the IPv6 header exists or where the UDP destination port is 66.
It will then check the application header value in the UDP payload and verify its last byte is a fixed value
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;}// The verifier has a special limit that it will not allow offsets// beyond 65535. We need this check (token_len > 64000) in order// to satisfy that, even though it is not possible.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;} -
Pass any packets that did not get dropped by program logic by returning
CF_EBPF_PASS.The currently supported return values are:
CF_EBPF_PASS = return value 0CF_EBPF_DROP = return value 1
The verifier, which runs when you upload a program to the API, will enforce that the program returns only known value types.
return CF_EBPF_PASS;
For reference, the example below is the basic program in its entirety:
#define CF_EBPF_HELPER_V0
#include <cf_ebpf_defs.h>#include <cf_ebpf_helper.h>
struct apphdr { uint8_t version; uint16_t length; // Length of the variable-length token unsigned char token[0]; // Variable-length token} __attribute__((packed));
uint64_tcf_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; }
// The verifier has a special limit that it will not allow offsets // beyond 65535. We need this check (token_len > 64000) in order // to satisfy that, even though it is not possible. 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;}The example program below implements a UDP-based challenge-response mechanism using helper functions to maintain state between packets from the same source IP. This is useful for mitigating DDoS attacks by requiring clients to prove they can receive and respond to challenges before allowing their traffic through.
The challenge mechanism works as follows:
When a packet arrives from an unknown source IP, the program generates a challenge packet containing a random nonce and marks the source IP as "challenged" in the state table. The original packet is dropped.
If a packet arrives from a source IP that has already been challenged, the program checks if the packet contains the correct challenge response (the nonce XORed with a secret value). If the response is correct, the source IP is marked as "verified". If incorrect, the source IP is immediately blocklisted.
Packets from verified source IPs are passed through without further checks.
-
Include the Cloudflare eBPF header files and define the helper version.
#define CF_EBPF_HELPER_V0#include <cf_ebpf_defs.h>#include <cf_ebpf_helper.h> -
Define constants for the challenge-response protocol.
The challenge response is computed by XORing the nonce with a secret value. The expiry time determines how long a challenged or verified status remains valid.
#define CHALLENGE_SECRET 0xDEADBEEFCAFEBABEULL#define CHALLENGE_EXPIRY_SECS 60#define VERIFIED_EXPIRY_SECS 3600 -
Define a structure for challenge packets.
The challenge packet contains the nonce that the client must respond to, and space for the client's response.
struct challenge_packet {uint64_t nonce; // Random nonce for this challengeuint64_t response; // Expected: nonce XOR CHALLENGE_SECRET}; -
Define the entry function and parse the packet.
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; -
Check the source IP status using
get_src_ip_status.The status indicates whether this source IP is new, challenged, verified, or blocklisted. The expiry timestamp indicates when the status expires.
uint8_t status;uint64_t expiry;int ret = get_src_ip_status(&status, &expiry);// Check if status has expiredint64_t now = timestamp();if (ret == 0 && expiry > 0 && (uint64_t)now > expiry) {// Status expired, treat as new connectionret = -1;} -
Handle verified source IPs.
The Programmable Flow Protection platform will drop packets from blocklisted IPs before the program is invoked. There is no need to explicitly handle the blocklisted case.
If the source IP has been verified (passed a previous challenge), allow the packet through.
if (ret == 0 && status == CF_EBPF_SRC_IP_STATUS_VERIFIED) {return CF_EBPF_PASS;} -
Check if this is a challenge response from a challenged source IP.
If the source IP was previously challenged, check if the current packet contains a valid challenge response. If the response is correct, mark the source IP as verified. If the response is incorrect, blocklist the source IP immediately.
if (ret == 0 && status == CF_EBPF_SRC_IP_STATUS_CHALLENGED) {// Get the stored nonce from user datauint64_t stored_nonce;if (get_src_ip_data(&stored_nonce) != 0) {return CF_EBPF_DROP;}// Parse the challenge response from the packet payloadstruct challenge_packet *resp = (struct challenge_packet *)(udp_hdr + 1);if ((uint8_t *)(resp + 1) > headers.data_end) {return CF_EBPF_DROP;}// Verify the response: should be nonce XOR secretuint64_t expected_response = stored_nonce ^ CHALLENGE_SECRET;if (resp->response == expected_response) {// Correct response - mark as verifiedset_src_ip_status(CF_EBPF_SRC_IP_STATUS_VERIFIED, VERIFIED_EXPIRY_SECS);set_src_ip_data(0); // Clear the noncereturn CF_EBPF_PASS;}// Wrong response - blocklist immediatelyset_src_ip_status(CF_EBPF_SRC_IP_STATUS_BLOCKLISTED, 0);return CF_EBPF_DROP;} -
Issue a new challenge for new source IPs.
Generate a random nonce, store it in the state table, create a challenge packet, and send it using
set_challenge.// Generate a new challenge for this source IPuint64_t nonce = rand();// Store the nonce and mark as challengedset_src_ip_status(CF_EBPF_SRC_IP_STATUS_CHALLENGED, CHALLENGE_EXPIRY_SECS);set_src_ip_data(nonce);// Build the challenge packet to send backstruct challenge_packet challenge;challenge.nonce = nonce;challenge.response = 0; // Client will fill this in// Set the challenge packet bufferset_challenge((uint8_t *)&challenge, sizeof(challenge));// Drop the original packet until client responds to challengereturn CF_EBPF_DROP;}
For reference, the example below is the complex program in its entirety:
#define CF_EBPF_HELPER_V0
#include <cf_ebpf_defs.h>#include <cf_ebpf_helper.h>
// Challenge-response protocol constants#define CHALLENGE_SECRET 0xDEADBEEFCAFEBABEULL#define CHALLENGE_EXPIRY_SECS 60#define VERIFIED_EXPIRY_SECS 3600
// Challenge packet structurestruct 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;
// Check source IP status uint8_t status; uint64_t expiry; int ret = get_src_ip_status(&status, &expiry);
// Check if status has expired int64_t now = timestamp(); if (ret == 0 && expiry > 0 && (uint64_t)now > expiry) { ret = -1; // Treat as new connection }
// Handle verified source IPs - allow through if (ret == 0 && status == CF_EBPF_SRC_IP_STATUS_VERIFIED) { return CF_EBPF_PASS; }
// Handle challenged source IPs - check for valid response 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; }
// Parse challenge response from packet payload struct challenge_packet *resp = (struct challenge_packet *)(udp_hdr + 1); if ((uint8_t *)(resp + 1) > headers.data_end) { return CF_EBPF_DROP; }
// Check response using XOR uint64_t expected_response = stored_nonce ^ CHALLENGE_SECRET; if (resp->response == expected_response) { // Correct response - mark as verified set_src_ip_status(CF_EBPF_SRC_IP_STATUS_VERIFIED, VERIFIED_EXPIRY_SECS); set_src_ip_data(0); return CF_EBPF_PASS; }
// Wrong response - blocklist immediately set_src_ip_status(CF_EBPF_SRC_IP_STATUS_BLOCKLISTED, 0); return CF_EBPF_DROP; }
// New source IP - issue initial challenge 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;}This program demonstrates several key concepts:
- State management: Using
get_src_ip_status,set_src_ip_status,get_src_ip_data, andset_src_ip_datato track the challenge state for each source IP. - Challenge emission: Using
set_challengeto send a challenge packet back to the client. - Cryptographic verification: Using a shared secret to verify that the client correctly responded to the challenge.
- Expiry handling: Using timestamps to expire stale state entries.
A helper function is a function provided by the Cloudflare runtime that a customer program calls.
Helper functions are crucial because the BPF Instruction Set Architecture (ISA) only supports certain system calls. For safety purposes, Cloudflare will only compile a BPF object file with a predetermined list of known libraries that a program developer cannot modify.
The table below provides a list of currently supported helper functions:
| Function name | Function signature | Description |
|---|---|---|
rand | uint64_t rand(void) | Generates a random unsigned integer. |
timestamp | uint64_t timestamp(void) | Returns the current timestamp. |
hash_md5 | int hash_md5(uint8_t *src, size_t src_len, uint8_t *dst) | Computes MD5 hash of the source buffer and stores result in destination buffer. |
hash_sha256 | int hash_sha256(uint8_t *src, size_t src_len, uint8_t *dst) | Computes SHA-256 hash of the source buffer and stores result in destination buffer. |
hash_sha512 | int hash_sha512(uint8_t *src, size_t src_len, uint8_t *dst) | Computes SHA-512 hash of the source buffer and stores result in destination buffer. |
hash_crc32 | int hash_crc32(uint8_t *src, size_t src_len, uint8_t *dst) | Computes CRC32 hash of the source buffer and stores result in destination buffer. |
hmac_sha256 | int hmac_sha256(uint8_t *key, size_t key_len, uint8_t *msg, size_t msg_len, uint8_t *dst) | Computes HMAC-SHA256 of the message using the provided key and stores result in destination buffer. |
hmac_sha512 | int hmac_sha512(uint8_t *key, size_t key_len, uint8_t *msg, size_t msg_len, uint8_t *dst) | Computes HMAC-SHA512 of the message using the provided key and stores result in destination buffer. |
set_challenge | int set_challenge(uint8_t *src, size_t src_len) | Sets challenge data for the current packet. |
get_src_ip_status | uint64_t get_src_ip_status(void) | Retrieves the status value associated with the source IP address from the state table. |
set_src_ip_status | int set_src_ip_status(uint64_t status) | Sets the status value associated with the source IP address in the state table. |
get_src_ip_data | int get_src_ip_data(uint8_t *dst, size_t dst_len) | Retrieves custom data associated with the source IP address from the state table. |
set_src_ip_data | int set_src_ip_data(uint8_t *src, size_t src_len) | Stores custom data associated with the source IP address in the state table. |
get_flow_data | int get_flow_data(uint8_t *dst, size_t dst_len) | Retrieves custom data associated with the current flow from the state table. |
set_flow_data | int set_flow_data(uint8_t *src, size_t src_len) | Stores custom data associated with the current flow in the state table. |
entropy | double entropy(uint8_t *src, size_t src_len) | Calculates the entropy of the source buffer. |
set_network_analytics_tag | int set_network_analytics_tag(Tag value) | Sets a custom tag for network analytics reporting. |
ntohs | uint16_t ntohs(uint16_t netshort) | Converts a 16-bit integer from network byte order to host byte order. |
htons | uint16_t htons(uint16_t hostshort) | Converts a 16-bit integer from host byte order to network byte order. |
ntohl | uint32_t ntohl(uint32_t netlong) | Converts a 32-bit integer from network byte order to host byte order. |
htonl | uint32_t htonl(uint32_t hostlong) | Converts a 32-bit integer from host byte order to network byte order. |
ntohll | uint64_t ntohll(uint64_t netlonglong) | Converts a 64-bit integer from network byte order to host byte order. |
htonll | uint64_t htonll(uint64_t hostlonglong) | Converts a 64-bit integer from host byte order to network byte order. |
parse_packet_data | int parse_packet_data(cf_ebpf_generic_ctx, cf_ebpf_packet_data, cf_ebpf_parsed_headers) | Use input cf_ebpf_generic_ctx and cf_ebpf_packet_data to generate valid cf_ebpf_parsed_headers.Upon success, cf_ebpf_parsed_headers will contain valid IP and UDP headers.Returns 0 on success or 1 on failure. |
With the exception of rand, timestamp, ntohs, htons, ntohl, htonl, ntohll, and htonll, all helper functions return a 0 on success and non-zero value on failure.
To upload a program, navigate to Networking > L3/4 DDoS protection > Advanced Protection in the Cloudflare dashboard. Then select the tab titled Programmable Flow Protection.
Under Programs, click the button "Upload new program." This will prompt you to select a file to upload with your C source code.
The Cloudflare API will receive the source code in the C file, compile it into BPF bytecode, and run the verifier against it.
If compilation or verification fails, the API will return a detailed error message.
If compilation and verification succeeds, Cloudflare will store the source code and object file to the account and return the program ID.
During the development process, you may find it useful to update the same program (identified by the same program ID) instead of repeatedly creating new programs as new resources.
To update the program, select the three dots next to your program. Then, select Overwrite. This will prompt you to choose a file to upload as your C source code.
To view all uploaded programs and their success statuses, view the table under the section entitled Programs.
To delete a program, select the three dots next to the program that you wish to delete. Then, select Delete.
Note that you will not be able to delete a program that is referenced in an active Rule.
Note that programs that have a "failed" status (meaning they failed to compile or pass verification) will be automatically and permanently deleted after 30 days of inactivity.
To create a rule, go to Networking > L3/4 DDoS protection > Advanced Protection in the Cloudflare dashboard. Then, select Programmable Flow Protection.
Under Rules, select Create rule. Fill out the corresponding fields of your new rule.
To view rules and their associated rule IDs, use the following GET endpoint.
curl https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/magic/programmable_flow_protection/configs/rules \--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN"To update an existing rule, use the following PATCH endpoint.
You can modify the mode, scope, and expression of an existing rule. The example below modifies an existing rule to make it run in disabled mode.
curl --request PATCH \https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/magic/programmable_flow_protection/configs/rules/$RULE_ID \--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \--header "Content-Type: application/json" \--data '{ "mode": "disabled"}'To delete an existing rule, use the following DELETE endpoint.
This does not delete the referenced program, but deletes the directive to execute the program.
curl --request DELETE \https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/magic/programmable_flow_protection/configs/rules/$RULE_ID \--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN"To delete all rules for an account, use the following DELETE endpoint.
This does not delete the referenced programs, but deletes the directive to execute all referenced programs.
curl --request DELETE \https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/magic/programmable_flow_protection/configs/rules \--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN"This API endpoint debugs a program by intaking:
- A local path to the input PCAP file provided as requested data in binary format. The input PCAP file has a maximum size limit of 5 MB and will be rejected if it is too large.
- An IP offset value provided as a query parameter. This is the number of bytes that the IP header is offset by in each packet of the input PCAP file.
For example, if the PCAP file captures Ethernet packets, the IP offset value would be 14. This endpoint assumes that all packets in a PCAP have the same IP offset value and will otherwise parse packets incorrectly. - The program ID provided in the request path.
This endpoint runs the referenced BPF program against the input PCAP and outputs a new annotated PCAP file. The output PCAP file will contain the exact same packets as the input PCAP file, and will also include the program verdict annotated in the Packet Comment section of each packet.
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.pcapThe Packet Comment annotation may contain:
- Program return value:
<value>.
CF_EBPF_PASScorrelates to0andCF_EBPF_DROPcorrelates to1. - Ignored, if it is not UDP.
You will want to safely deploy and test programs without impacting existing production traffic. An initial deployment approach could be to set a global scoped rule to disabled and set a colo or region level scoped rule to monitoring with a filter expression only acting on some subset of IP traffic.
Each Cloudflare region or colo will apply the most granular rule. So, in the scenario described above, the colos or regions specified in the monitoring rule will execute the developer program in monitoring mode, while every other Cloudflare location will not execute the program at all. The monitoring rule would only execute on traffic that matches the filter expression.
Then, after verifying the correct behavior with Network Analytics, you can update and expand the monitoring rule's scope and filter expression. Eventually, you can delete the disabled and monitoring rules and apply a global enabled rule.
Using the Expression field to limit programs to a subset of IPs or prefixes and the Mode field to dictate whether a program actually drops packets ensures a program's safety and granularity upon rollout.
Traffic flowing through Programmable Flow Protection can be found in the Network Analytics dashboard.
You can use the Cloudflare GraphQL API to granularly query traffic data in the programmableFlowProtectionNetworkAnalyticsAdaptiveGroups group.
For example, the curl command below executes a query that shows the total sum of bits and packets that went through Programmable Flow Protection in a time frame.
$CLOUDFLARE_API_TOKEN and <ACCOUNT_TAG> must be changed to correlate to the user's account.
Cloudflare recommends using a client like GraphQL to explore all the dimensions and fields available for querying in programmableFlowProtectionNetworkAnalyticsAdaptiveGroups.
echo '{ "query": "query PFPActivity { viewer { accounts(filter: { accountTag: \"<ACCOUNT_TAG>\" }) { programmableFlowProtectionNetworkAnalyticsAdaptiveGroups( filter: { datetime_geq: \"2025-12-03T11:00:00Z\" datetime_leq: \"2025-12-04T11:10:00Z\" } limit: 10 ) { sum { bits packets } } } } }"}' | tr -d '\n' | curl --silent \https://api.cloudflare.com/client/v4/graphql \--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \--header "Content-Type: application/json" \--data @-{ "data": { "viewer": { "accounts": [ { "programmableFlowProtectionNetworkAnalyticsAdaptiveGroups": [ { "sum": { "bits": 16680384000, "packets": 23020000 } } ] } ] } }, "errors": null}/** cf_ebpf_generic_ctx is passed into the BPF program*/struct cf_ebpf_generic_ctx{ /* Pointer to the beginning of the context data. */ uint64_t data; /* Pointer to the end of the context data. */ uint64_t data_end; /* Space for the program to store metadata. */ uint64_t meta_data;};
/** cf_ebpf_packet_data_v1 is passed into the BPF program*/struct cf_ebpf_packet_data { /* Total length of the packet. */ size_t total_packet_length; /* Size of the IP header. Supports IPv4 (including options) and IPv6. */ size_t ip_header_length; /* Bytes of the packet, starting with the IP header. */ uint8_t packet_buffer[1500];};
/** cf_ebpf_parsed_headers can be populated from cf_ebpf_generic_ctx and* cf_ebpf_packet_data in the BPF program*/struct cf_ebpf_parsed_headers { /* Pointer to the parsed IPv4 header, if present (otherwise null). */ struct iphdr *ipv4; /* Pointer to the parsed IPv6 header, if present (otherwise null). */ struct ipv6hdr *ipv6; /* Pointer to the parsed UDP header. */ struct udphdr *udp; /* Raw pointer to the last valid byte of the packet context data. */ uint8_t *data_end;};
/** IPv4 header, used as field of cf_ebpf_parsed_headers */* source: https://github.com/torvalds/linux/blob/a7423e6ea2f8f6f453de79213c26f7a36c86d9a2/include/uapi/linux/ip.h#L87*/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 header, used as field of cf_ebpf_parsed_headers* source: https://github.com/torvalds/linux/blob/a7423e6ea2f8f6f453de79213c26f7a36c86d9a2/include/uapi/linux/ipv6.h#L118*/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 header, used as field of cf_ebpf_parsed_headers* source: https://github.com/torvalds/linux/blob/a7423e6ea2f8f6f453de79213c26f7a36c86d9a2/include/uapi/linux/udp.h#L23*/struct udphdr { uint16_t source; uint16_t dest; uint16_t len; uint16_t check;};
/* Function to construct cf_ebpf_parsed_headers from cf_ebpf_generic_ctx and* cf_ebpf_packet_data_v1. Performs required memory checks to pass verifier.* Returns 0 on success and 1 on failure (e.g., packet too short, invalid length).* cf_ebpf_packet_data_v1 is filled with IP and UDP header data on success.*/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);
/* Returns a random unsigned integer value. */uint64_t rand(void);
/* Returns the current timestamp. */uint64_t timestamp(void);
/* Computes MD5 hash of the source buffer and stores result in destination buffer. */int hash_md5(uint8_t *src, size_t src_len, uint8_t *dst);
/* Computes SHA-256 hash of the source buffer and stores result in destination buffer. */int hash_sha256(uint8_t *src, size_t src_len, uint8_t *dst);
/* Computes SHA-512 hash of the source buffer and stores result in destination buffer. */int hash_sha512(uint8_t *src, size_t src_len, uint8_t *dst);
/* Computes CRC32 hash of the source buffer and stores result in destination buffer. */int hash_crc32(uint8_t *src, size_t src_len, uint8_t *dst);
/* Computes HMAC-SHA256 of the message using the provided key and stores result in destination buffer. */int hmac_sha256(uint8_t *key, size_t key_len, uint8_t *msg, size_t msg_len, uint8_t *dst);
/* Computes HMAC-SHA512 of the message using the provided key and stores result in destination buffer. */int hmac_sha512(uint8_t *key, size_t key_len, uint8_t *msg, size_t msg_len, uint8_t *dst);
/* Sets challenge data for the current packet. */int set_challenge(uint8_t *src, size_t src_len);
/* Retrieves the status value associated with the source IP address from the state table. */uint64_t get_src_ip_status(void);
/* Sets the status value associated with the source IP address in the state table. */int set_src_ip_status(uint64_t status);
/* Retrieves custom data associated with the source IP address from the state table. */int get_src_ip_data(uint8_t *dst, size_t dst_len);
/* Stores custom data associated with the source IP address in the state table. */int set_src_ip_data(uint8_t *src, size_t src_len);
/* Retrieves custom data associated with the current flow from the state table. */int get_flow_data(uint8_t *dst, size_t dst_len);
/* Stores custom data associated with the current flow in the state table. */int set_flow_data(uint8_t *src, size_t src_len);
/* Calculates the entropy of the source buffer. */double entropy(uint8_t *src, size_t src_len);
/* Sets a custom tag for network analytics reporting. */int set_network_analytics_tag(uint64_t tag);
/* Converts a 16-bit integer from network byte order to host byte order. */uint16_t ntohs(uint16_t netshort);
/* Converts a 16-bit integer from host byte order to network byte order. */uint16_t htons(uint16_t hostshort);
/* Converts a 32-bit integer from network byte order to host byte order. */uint32_t ntohl(uint32_t netlong);
/* Converts a 32-bit integer from host byte order to network byte order. */uint32_t htonl(uint32_t hostlong);
/* Converts a 64-bit integer from network byte order to host byte order. */uint64_t ntohll(uint64_t netlonglong);
/* Converts a 64-bit integer from host byte order to network byte order. */uint64_t htonll(uint64_t hostlonglong);