INTEGRITY Dokumentace

Programmable Flow Protection (Beta)

Programmable Flow Protection je systém ochrany před DDoS útoky vedenými přes vlastní i standardizované protokoly vrstvy 7 postavené na UDP, například herní protokoly, protokoly finančních služeb, VoIP, telekomunikace a streaming. Z hlediska topologie podporuje asymetrické i symetrické konfigurace, kontroluje ale pouze příchozí provoz.

Programmable Flow Protection je momentálně v uzavřené beta verzi a je k dispozici jako doplněk k Magic Transit (BYOIP nebo IP adresy pronajaté od Cloudflare). Systém je k dispozici pouze pro tuto službu. Pokud jej chcete zapnout, obraťte se na svůj tým pro správu účtu nebo vyplňte tento formulář.

Jak to funguje

Systém Programmable Flow Protection umožňuje napsat vlastní stavový program pracující na úrovni paketů v jazyce C a spustit jej v globální anycast síti Cloudflare jako program extended Berkeley Packet Filter (eBPF) běžící v uživatelském prostoru. program eBPF je systém pro filtrování paketů, který vývojáři umožňuje psát vlastní výkonnou síťovou logiku.

Programmable Flow Protection kontroluje a analyzuje protokoly vaší aplikace postavené na UDP (hloubková inspekce paketů) a podle vašeho programu rozhoduje, jak s pakety naloží. Logika vlastního programu vám umožní propouštět oprávněné uživatele a zároveň aktivně blokovat útoky.

Systém je postaven na flowtrackd platformě, stavové mitigační platformě Cloudflare. Systém Programmable Flow Protection staví na systému DDoS Advanced Protection a jeho obecná nastavení ke svému fungování. Respektuje prefixy které jste vybrali ke směrování přes systémy Advanced Protection, a také allowlist. Systém Advanced DDoS Protection by měl být povoleno pro fungování systému Programmable Flow Protection.

V průběhu bety Cloudflare uživatelům s psaním vlastního kódu pomáhá a poskytuje jim doporučení. Hotové ukázky kódu (šablony) pro běžné herní protokoly a protokoly VoIP mohou přibýt později.


Začínáme

Jakmile máte pro svůj účet zapnutou Programmable Flow Protection, přejděte na Sítě > L3/4 DDoS Protection > Advanced Protection v dashboardu Cloudflare. V části Programmable Flow Protection kartě:

  1. Nahrajte svůj program eBPF napsaný v jazyce C.

    Systém program ověří a uloží jej do vašeho účtu. Rozhraní API program zkompiluje a nad zkompilovaným programem poté spustí verifikátor, který vynutí kontroly paměti a ověří, že se program ukončí. Pokud program neprojde kompilací nebo ověřením, vrátí Cloudflare dashboard podrobnou chybovou zprávu.

  2. Vytvořte pravidlo

  3. Chování programu ověříte na dashboardu Network Analytics, kde vyberete Programmable Flow Protection kartě.

Můžete vytvořit další pravidla s odlišnou hodnotou parametru nastavení pravidla s vymezeným rozsahem do různých regionů a lokalit Cloudflare, čímž změníte režim (Mitigation nebo Monitoring) tak, aby odpovídal vašim vzorcům provozu a obchodním potřebám.

Systém Programmable Flow Protection podporuje Data Localization suite.

Napsání základního programu

Následující kroky vytvoří ukázkový program, který zahazuje veškerý provoz protokolu User Datagram Protocol (UDP) s hlavičkou IPv6. Zahazuje také provoz směřovaný na port 66 a provoz, který v datové části UDP nemá určitou vlastní hodnotu aplikační hlavičky.

  1. Přidejte direktivu define, která určí použité verzované pomocné funkce.

    S tím, jak bude Cloudflare do rozhraní Programmable Flow Protection API přidávat další funkce, budeme vydávat jeho nové verze. Zpětná kompatibilita verzí je zaručena.

    #define CF_EBPF_HELPER_V0
  2. Vložte hlavičkové soubory Cloudflare eBPF.

    Tyto soubory mají pomocné funkce k analýze vstupních dat paketu předaných BPF programu.

    #include <cf_ebpf_defs.h>
    #include <cf_ebpf_helper.h>
  3. Definujte vstupní funkci pro zpracování paketů.

    Aby program prošel ověřením u Cloudflare, musí mít přesně tuto signaturu funkce.

    Návratový typ uint64_t určuje, zda Cloudflare paket propustí, nebo zahodí. Název funkce cf_ebpf_main se používá jako vstupní bod programu. Argument void *state označuje data, která Cloudflare předává na vstup vašemu programu BPF.

    uint64_t cf_ebpf_main(void *state)
  4. Přetypuje vstupní argument na použitelné struktury.

    Převeďte vstupní data na cf_ebpf_generic_ctx, který Cloudflare sděluje hranice dat v paměti, ze které čteme.

    Dále deklarujte proměnné pro parsování dat. cf_ebpf_parsed_headers bude obsahovat hlavičky IPv4, IPv6 a UDP. cf_ebpf_packet_data bude obsahovat kopii původního IP paketu přijatého sítí Cloudflare (maximálně 1,500 bajtů) a dále délku paketu a délku IP hlavičky.

    struct cf_ebpf_generic_ctx *ctx = state;
    struct cf_ebpf_parsed_headers headers;
    struct cf_ebpf_packet_data *p;
  5. Proměnné naplňte voláním pomocné funkce.

    Proměnné musíte doplnit voláním pomocné funkce parse_packet_data, který Cloudflare poskytuje v hlavičkovém souboru přiloženém v kroku 2.

    parse_packet_data provádí kontroly paměti nutné k tomu, aby program prošel verifikátorem. Funkce parse_packet_data vrací 0 při úspěchu. Pokud volání uspěje, jsou vstupní parametry správně vyplněné. parse_packet_data vrací 1 při selhání. Pokud parse_packet_data selže, program musí vrátit CF_EBPF_DROP k zahození paketu, aby program prošel verifierem.

    if (parse_packet_data(ctx, &p, &headers) != 0) {
        return CF_EBPF_DROP;
    }

    Hodnoty dostupné po úspěšném zpracování:

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

    Úplnou definici pomocných funkcí a struktur uvádí Pomocné funkce a struktury BPF.

  6. Napište vlastní logiku.

    V předchozích krocích jsme sestavili kód, který zůstává stejný pro každý program, který napíšete, bez ohledu na jeho logiku.

    Nyní můžete napsat vlastní logiku.

    V ukázce níže program zahodí každý paket, který obsahuje hlavičku IPv6 nebo má cílový UDP port 66.

    Následně zkontroluje hodnotu aplikační hlavičky v UDP payloadu a ověří, že jejím posledním bajtem je pevná hodnota 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;
     }
  7. Pakety, které logika programu nezahodila, propustíte tím, že vrátíte CF_EBPF_PASS.

    Aktuálně jsou podporovány tyto návratové hodnoty:

    • CF_EBPF_PASS = return value 0
    • CF_EBPF_DROP = return value 1

    Verifikátor, který se spustí při nahrání programu do rozhraní API, vynutí, aby program vracel pouze známé typy hodnot.

    return CF_EBPF_PASS;

Pro přehlednost uvádíme níže celý základní program:

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

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

Napsání složitějšího programu: odpověď založená na výzvě

Ukázkový program níže implementuje mechanismus výzvy a odpovědi nad UDP a pomocí pomocných funkcí udržuje stav mezi pakety ze stejné zdrojové IP adresy. Hodí se ke zmírňování útoků DDoS: klient musí nejprve prokázat, že výzvu přijal a dokáže na ni odpovědět, teprve pak systém jeho provoz propustí.

Mechanismus výzvy funguje takto:

Když dorazí paket z neznámé zdrojové IP adresy, program vygeneruje výzvový paket s náhodným nonce a zdrojovou IP adresu označí ve stavové tabulce jako "challenged". Původní paket se zahodí.

Pokud paket dorazí ze zdrojové IP adresy, které už byla výzva odeslána, program ověří, zda obsahuje správnou odpověď na výzvu (hodnota nonce zkombinovaná operací XOR s tajnou hodnotou). Při správné odpovědi se zdrojová IP adresa označí jako „ověřená“, při nesprávné se okamžitě přidá na blocklist.

Pakety z ověřených zdrojových IP adres projdou bez dalších kontrol.

  1. Vložte hlavičkové soubory Cloudflare eBPF a definujte verzi helperu.

    #define CF_EBPF_HELPER_V0
    
    #include <cf_ebpf_defs.h>
    #include <cf_ebpf_helper.h>
  2. Definujte konstanty protokolu výzva-odpověď.

    Odpověď na výzvu vznikne operací XOR mezi hodnotou nonce a tajnou hodnotou. Doba platnosti určuje, jak dlouho zůstává stav challenged nebo verified v platnosti.

    #define CHALLENGE_SECRET 0xDEADBEEFCAFEBABEULL
    #define CHALLENGE_EXPIRY_SECS 60
    #define VERIFIED_EXPIRY_SECS 3600
  3. Definujte strukturu paketů s výzvou.

    Paket s výzvou obsahuje nonce, na který musí klient odpovědět, a místo pro jeho odpověď.

    struct challenge_packet {
        uint64_t nonce;        // Random nonce for this challenge
        uint64_t response;     // Expected: nonce XOR CHALLENGE_SECRET
    };
  4. Definujte vstupní funkci a zpracujte paket.

    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. Stav zdrojové IP adresy ověříte pomocí get_src_ip_status.

    Stav udává, zda je tato zdrojová IP adresa nová, vyzvaná, ověřená, nebo na seznamu blokovaných. Časové razítko vypršení udává, kdy platnost stavu skončí.

        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) {
            // Status expired, treat as new connection
            ret = -1;
        }
  6. Zpracujte ověřené zdrojové IP adresy.

    Platforma Programmable Flow Protection zahodí pakety z IP adres na blokovacím seznamu ještě předtím, než se program spustí. Tento případ proto ve svém kódu nemusíte nijak ošetřovat.

    Pokud je zdrojová IP adresa ověřená, tedy prošla dřívější výzvou, paket propusťte.

        if (ret == 0 && status == CF_EBPF_SRC_IP_STATUS_VERIFIED) {
            return CF_EBPF_PASS;
        }
  7. Zjistí, zda jde o odpověď na výzvu ze zdrojové IP adresy, které byla výzva odeslána.

    Pokud už zdrojová IP adresa výzvu dostala, zkontrolujte, zda aktuální paket obsahuje platnou odpověď. Při správné odpovědi označte zdrojovou IP adresu jako ověřenou, při nesprávné ji okamžitě přidejte na blocklist.

        if (ret == 0 && status == CF_EBPF_SRC_IP_STATUS_CHALLENGED) {
            // Get the stored nonce from user data
            uint64_t stored_nonce;
            if (get_src_ip_data(&stored_nonce) != 0) {
                return CF_EBPF_DROP;
            }
    
            // Parse the challenge response from the packet payload
            struct 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 secret
            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);  // Clear the nonce
                return CF_EBPF_PASS;
            }
    
            // Wrong response - blocklist immediately
            set_src_ip_status(CF_EBPF_SRC_IP_STATUS_BLOCKLISTED, 0);
            return CF_EBPF_DROP;
        }
  8. Vydá novou výzvu pro nové zdrojové IP adresy.

    Vygenerujte náhodný nonce, uložte jej do stavové tabulky, vytvořte challenge paket a odešlete jej pomocí set_challenge.

        // Generate a new challenge for this source IP
        uint64_t nonce = rand();
    
        // Store the nonce and mark as challenged
        set_src_ip_status(CF_EBPF_SRC_IP_STATUS_CHALLENGED, CHALLENGE_EXPIRY_SECS);
        set_src_ip_data(nonce);
    
        // Build the challenge packet to send back
        struct challenge_packet challenge;
        challenge.nonce = nonce;
        challenge.response = 0;  // Client will fill this in
    
        // Set the challenge packet buffer
        set_challenge((uint8_t *)&challenge, sizeof(challenge));
    
        // Drop the original packet until client responds to challenge
        return CF_EBPF_DROP;
    }

Pro přehlednost uvádíme níže celý složitý program:

#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 structure
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;

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

Tento program ukazuje několik klíčových principů:

Napsání složitějšího programu: omezování rychlosti

Ukázkový program níže implementuje omezovač rychlosti pro jednotlivé zdrojové IP adresy pomocí algoritmu pevného okna. Hodí se ke zmírňování objemových útoků DDoS, protože omezuje, kolik paketů může jedna zdrojová IP adresa odeslat v daném časovém okně.

Mechanismus omezování rychlosti funguje takto:

Po příchodu paketu načte program uložený stav pro danou zdrojovou IP adresu. Stav obsahuje časové razítko začátku okna a čítač paketů, obojí sbalené do jediné 64bitové hodnoty. Pokud aktuální čas stále spadá do okna, čítač se zvýší. Jakmile čítač překročí nastavený limit, paket se zahodí. Po vypršení okna se čítač vynuluje.

  1. Vložte hlavičkové soubory Cloudflare eBPF a definujte verzi helperu.

    #include <cf_ebpf_defs.h>
    #define CF_EBPF_HELPER_V0
    #include <cf_ebpf_helper.h>
  2. Definujte konstanty pro konfiguraci rate limitu.

    RATE_LIMIT nastavuje maximální počet paketů povolených za okno. WINDOW_SECONDS určuje délku každého časového okna v sekundách.

    #define RATE_LIMIT 100         // Maximum packets allowed per window
    #define WINDOW_SECONDS 60      // Time window in seconds
  3. Definujte makra pro zabalení a rozbalení stavových dat.

    Stavová tabulka zdrojových IP adres ukládá jeden u64 hodnotu na jednu zdrojovou IP adresu. Chcete-li sledovat zároveň časové razítko i čítač, uložte je do jedné hodnoty: časové razítko do horních 32 bitů a čítač do dolních 32 bitů.

    #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. Definujte vstupní funkci a získejte aktuální časové razítko.

    Pokud pomocná funkce pro časový údaj selže, paket propusťte, abyste předešli falešně pozitivním detekcím.

    uint64_t cf_ebpf_main(void *state)
    {
        // Get current timestamp
        int64_t now = timestamp();
        if (now < 0) {
            return CF_EBPF_PASS; // If timestamp fails, allow the packet
        }
        uint32_t now_secs = (uint32_t)now;
  5. Načte existující stav pro tuto zdrojovou IP adresu.

    Použijte get_src_ip_data k ověření, zda už byla tato zdrojová IP adresa dříve zaznamenána.

        // Try to get existing state for this source IP
        uint64_t data;
        int ret = get_src_ip_data(&data);
        uint32_t window_start;
        uint32_t counter;
  6. Ošetřete případ, kdy jde o novou zdrojovou IP adresu.

    Pokud žádný záznam neexistuje (návratová hodnota je -1), jde o první paket z této zdrojové IP. Inicializujte okno tak, aby začínalo nyní, s čítačem 1.

        if (ret == -1) {
            // No existing entry - first packet from this IP
            // Initialize: window starts now, counter = 1
            window_start = now_secs;
            counter = 1;
        }
  7. Zpracujte již známé zdrojové IP adresy a zkontrolujte časové okno.

    Pokud záznam existuje, rozbalte uložený časový údaj a čítač. Pokud okno vypršelo, obě hodnoty vynulujte. V opačném případě čítač zvyšte a zkontrolujte, zda nepřekročil limit.

        } else if (ret != 0) {
            // If there's other unknown error with getting src_ip_data, pass packet
            return CF_EBPF_PASS;
        } else {
            // Entry exists - unpack the state
            window_start = UNPACK_TIMESTAMP(data);
            counter = UNPACK_COUNTER(data);
            // Check if we're still in the same time window
            if (now_secs - window_start >= WINDOW_SECONDS) {
                // Window expired - reset counter and start new window
                window_start = now_secs;
                counter = 1;
            } else {
                // Still in same window - increment counter
                counter++;
                // Check if rate limit exceeded
                if (counter > RATE_LIMIT) {
                    // Drop packet without updating state
                    return CF_EBPF_DROP;
                }
            }
        }
  8. Uložte aktualizovaný stav a paket propusťte.

    Časovou značku začátku okna a čítač zabalte zpět do jedné hodnoty a tu uložte do stavové tabulky zdrojových IP adres.

        // Store updated state
        uint64_t new_data = PACK_STATE(window_start, counter);
        set_src_ip_data(new_data);
        return CF_EBPF_PASS;
    }

Pro přehlednost uvádíme níže celý program pro omezení rychlosti:

#include <cf_ebpf_defs.h>
#define CF_EBPF_HELPER_V0
#include <cf_ebpf_helper.h>

// Rate limit configuration
// This program implements a fixed (not sliding) window ratelimit.
#define RATE_LIMIT 100         // Maximum packets allowed per window
#define WINDOW_SECONDS 60      // Time window in seconds

// The source IP table holds a mapping from source IP -> custom u64. We will make the custom u64 value in the 
// table hold a timestamp and a counter to accomplish a ratelimit.
// 
// NOTE: the source IP table is effectively a LRU cache. If it is full, old values will be evicted. 
// Values are also garbage collected from the table every 1hr.
// 
// The macros below pack the timestamp (upper 32 bits) and counter (lower 32 bits) into 64-bit data
// into a value that we can store into the source IP table.
#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)
{
    // Get current timestamp
    int64_t now = timestamp();
    if (now < 0) {
        return CF_EBPF_PASS; // If timestamp fails, allow the packet
    }
    uint32_t now_secs = (uint32_t)now;

    // Try to get existing state for this source IP
    uint64_t data;
    int ret = get_src_ip_data(&data);
    uint32_t window_start;
    uint32_t counter;

    if (ret == -1) {
        // No existing entry - first packet from this IP
        // Initialize: window starts now, counter = 1
        window_start = now_secs;
        counter = 1;
    } else if (ret != 0) {
        // If there's other unknown error with getting src_ip_data, pass packet
        return CF_EBPF_PASS;
    } else {
        // Entry exists - unpack the state
        window_start = UNPACK_TIMESTAMP(data);
        counter = UNPACK_COUNTER(data);
        // Check if we're still in the same time window
        if (now_secs - window_start >= WINDOW_SECONDS) {
            // Window expired - reset counter and start new window
            window_start = now_secs;
            counter = 1;
        } else {
            // Still in same window - increment counter
            counter++;
            // Check if rate limit exceeded
            if (counter > RATE_LIMIT) {
                // Drop packet without updating state
                // Here is where the actual ratelimit occurs.
                return CF_EBPF_DROP;
            }
        }
    }
    // Store updated state
    uint64_t new_data = PACK_STATE(window_start, counter);
    set_src_ip_data(new_data);
    return CF_EBPF_PASS;
}

Tento program ukazuje několik klíčových principů:


Stav

Každý program má přístup ke svému vlastnímu lokálnímu stavu. Stav je lokální pro každý server a mezi datovými centry se nesdílí.

Stav je vázaný na konkrétní program. Když změníte režim pravidla (disabled, monitoring nebo enabled), obsah stavových tabulek zůstane zachovaný. Pokud ale změníte program pravidla nebo obsah samotného programu, stavové tabulky se vymažou.

Vašemu programu jsou k dispozici dvě stavové tabulky.

Stavová tabulka zdrojových IP adres

Stavová tabulka zdrojových IP adres ukládá stav podle klíče, kterým je zdrojová IP adresa. Každý záznam obsahuje:

Pole Typ Popis
Stav Enum Stav zdrojové IP adresy: None (0), Challenged (1), Verified (2) nebo Blocklisted (3).
Uživatelská data u64 Uživatelsky definovaná hodnota, kterou můžete nastavit k libovolnému účelu.

Výchozí maximální kapacita je 1 000 záznamů.

K práci s touto tabulkou použijte následující pomocné funkce:

Záznam v tabulce stavů zdrojových IP adres vzniká za těchto podmínek:

  1. program volá set_src_ip_status k označení zdrojové IP adresy jako Challenged, Verified nebo Blocklisted.
  2. program volá set_src_ip_data k uložení vlastních u64 dat pro zdrojovou IP adresu.
  3. program volá set_challenge pro novou zdrojovou IP, která v tabulce dosud nemá záznam.

Tabulka stavů toků

Stavová tabulka toků ukládá stav pod klíčem tvořeným čtveřicí: zdrojová IP, zdrojový port, cílová IP a cílový port. Každý záznam obsahuje u64 hodnota, kterou můžete nastavit k libovolnému účelu.

Výchozí maximální kapacita je 10 000 záznamů.

K práci s touto tabulkou použijte následující pomocné funkce:

Záznam v tabulce stavů toků vzniká za těchto podmínek:

  1. program volá set_flow_data k uložení vlastních u64 dat pro daný tok.

Chování cache

Obě stavové tabulky jsou cache typu LRU (least recently used). Jakmile tabulka dosáhne maximální kapacity, nejstarší záznam se zahodí, aby uvolnil místo novým. Záznamy se navíc uvolňují, pokud k nim hodinu nikdo nepřistoupil.


Pomocné funkce a struktury BPF

Pomocná funkce je funkce, kterou poskytuje běhové prostředí Cloudflare a kterou volá zákaznický program.

Pomocné funkce jsou zásadní, protože architektura instrukční sady BPF (ISA) podporuje jen některá systémová volání. Z bezpečnostních důvodů Cloudflare zkompiluje objektový soubor BPF pouze s předem daným seznamem známých knihoven, který vývojář programu nemůže změnit.

Definice pomocných funkcí a zdrojové kódy obalu verifikátoru najdete na GitHub.

Pomocné funkce

parse_packet_data

Vytvoří cf_ebpf_parsed_headers z cf_ebpf_generic_ctx a cf_ebpf_packet_data. Provádí povinné kontroly paměti, aby program prošel verifikátorem.

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

Argumenty:

Vrací: 0 při úspěchu, 1 při selhání (například příliš krátký paket nebo neplatná délka). Při úspěchu out_headers obsahuje platné ukazatele na hlavičky IP a UDP.

rand

Vygeneruje náhodné celé číslo bez znaménka.

uint64_t rand(void);

Vrací: Náhodný uint64_t hodnota.

timestamp

Vrátí aktuální UNIX timestamp (počet nepřestupných sekund od 1. ledna 1970 0:00:00 UTC).

int64_t timestamp(void);

Vrací: Aktuální časové razítko jako int64_t.

hash_md5

Vypočítá hash MD5 zdrojového bufferu a výsledek uloží do cílového bufferu.

int hash_md5(uint8_t *src, size_t src_len, uint8_t *dest, size_t dest_len);

Argumenty:

Vrací:

hash_sha256

Vypočítá hash SHA-256 zdrojového bufferu a výsledek uloží do cílového bufferu.

int hash_sha256(uint8_t *src, size_t src_len, uint8_t *dest, size_t dest_len);

Argumenty:

Vrací:

hash_sha512

Vypočítá hash SHA-512 zdrojového bufferu a výsledek uloží do cílového bufferu.

int hash_sha512(uint8_t *src, size_t src_len, uint8_t *dest, size_t dest_len);

Argumenty:

Vrací:

hash_crc32

Vypočítá hash CRC32 zdrojového bufferu a výsledek uloží jako 64bitové celé číslo. Jde o pomocnou funkci, která si interně zajišťuje převod z bajtů na celé číslo.

int hash_crc32(uint8_t *src, size_t src_len, uint64_t *dest);

Argumenty:

Vrací:

hash_blake2b512

Vypočítá hash BLAKE2B-512 zdrojového bufferu a výsledek uloží do cílového bufferu.

int hash_blake2b512(const uint8_t *src, size_t src_len, uint8_t *dest, size_t dest_len);

Argumenty:

Vrací:

hmac_sha256

Vypočítá HMAC-SHA256 zdrojového bufferu a výsledek uloží do cílového bufferu. Privátní klíč se nastavuje na úrovni platformy a program BPF k němu nemá přímý přístup. Klíč je jedinečný pro každý server a každého zákazníka.

int hmac_sha256(uint8_t *src, size_t src_len, uint8_t *dest, size_t dest_len);

Argumenty:

Vrací:

hmac_sha512

Vypočítá HMAC-SHA512 zdrojového bufferu a výsledek uloží do cílového bufferu. Privátní klíč se nastavuje na úrovni platformy a program BPF k němu nemá přímý přístup. Klíč je jedinečný pro každý server a každého zákazníka.

int hmac_sha512(uint8_t *src, size_t src_len, uint8_t *dest, size_t dest_len);

Argumenty:

Vrací:

hmac_blake2b512

Vypočítá BLAKE2B-512 HMAC zdrojového bufferu a výsledek uloží do cílového bufferu. Privátní klíč se nastavuje na úrovni platformy a program BPF k němu nemá přímý přístup. Klíč je jedinečný pro každý server a každého zákazníka.

int hmac_blake2b512(const uint8_t *src, size_t src_len, uint8_t *dest, size_t dest_len);

Argumenty:

Vrací:

set_challenge

Nastaví data výzvy pro aktuální paket. Použijte, když chcete klientovi odeslat paket s výzvou.

int set_challenge(uint8_t *src, size_t src_len);

Argumenty:

Vrací:

get_src_ip_status

Načte ze stavové tabulky hodnotu status spojenou se zdrojovou IP adresou.

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

Argumenty:

Vrací:

set_src_ip_status

Nastaví hodnotu stavu přiřazenou zdrojové IP adrese ve stavové tabulce.

int set_src_ip_status(uint8_t status, uint64_t expiry_secs);

Argumenty:

Vrací:

get_src_ip_data

Načte ze stavové tabulky vlastní data spojená se zdrojovou IP adresou.

int get_src_ip_data(uint64_t *data);

Argumenty:

Vrací:

set_src_ip_data

Ukládá do stavové tabulky vlastní data přiřazená ke zdrojové IP adrese.

int set_src_ip_data(uint64_t data);

Argumenty:

Vrací:

get_flow_data

Načte ze stavové tabulky vlastní data spojená s aktuálním tokem.

int get_flow_data(uint64_t *data);

Argumenty:

Vrací:

set_flow_data

Ukládá do stavové tabulky vlastní data přiřazená k aktuálnímu toku.

int set_flow_data(uint64_t data);

Argumenty:

Vrací:

entropy

Vypočítá Shannonovu entropii zdrojového bufferu. Výsledek se vrací v milibitech, v rozsahu od 0 (všechny bajty stejné) do 8000 (všech 256 hodnot bajtů rovnoměrně rozložených).

int64_t entropy(uint8_t *src, size_t src_len);

Argumenty:

Vrací:

set_network_analytics_tag

Nastaví vlastní značku pro reporting v Network Analytics. Značka se zobrazí spolu se vzorkem paketu v dashboardu Network Analytics. Ve výchozím nastavení se pakety vzorkují v poměru 1/10,000. Během jednoho spuštění programu se nastaví jen jedna značka. Pokud spuštění programu volá set_network_analytics_tag vícekrát, na vzorek paketu se použije hodnota posledního tagu.

int set_network_analytics_tag(uint64_t tag);

Argumenty:

Vrací: 0 při úspěchu.

ntohs

Převede 16bitové celé číslo ze síťového pořadí bajtů na pořadí bajtů hostitele.

uint16_t ntohs(uint16_t netshort);

Argumenty:

Vrací: Hodnota v pořadí bajtů hostitele.

htons

Převede 16bitové celé číslo z pořadí bajtů hostitele na síťové pořadí bajtů.

uint16_t htons(uint16_t hostshort);

Argumenty:

Vrací: Hodnota v síťovém pořadí bajtů.

ntohl

Převede 32bitové celé číslo ze síťového pořadí bajtů na pořadí bajtů hostitele.

uint32_t ntohl(uint32_t netlong);

Argumenty:

Vrací: Hodnota v pořadí bajtů hostitele.

htonl

Převede 32bitové celé číslo z pořadí bajtů hostitele na síťové pořadí bajtů.

uint32_t htonl(uint32_t hostlong);

Argumenty:

Vrací: Hodnota v síťovém pořadí bajtů.

ntohll

Převede 64bitové celé číslo ze síťového pořadí bajtů na pořadí bajtů hostitele.

uint64_t ntohll(uint64_t netlonglong);

Argumenty:

Vrací: Hodnota v pořadí bajtů hostitele.

htonll

Převede 64bitové celé číslo z pořadí bajtů hostitele na síťové pořadí bajtů.

uint64_t htonll(uint64_t hostlonglong);

Argumenty:

Vrací: Hodnota v síťovém pořadí bajtů.

Struktury

cf_ebpf_generic_ctx

Obecná struktura kontextu předávaná programu BPF.

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

Obsahuje nezpracovaná data paketu předaná programu BPF.

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

Obsahuje ukazatele na parsované hlavičky IP a UDP. Plní se voláním parse_packet_data.

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

iphdr

Struktura hlavičky IPv4. Zdroj: Jádro Linuxu.

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

Struktura hlavičky IPv6. Zdroj: Jádro Linuxu.

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

Struktura hlavičky UDP. Zdroj: Jádro Linuxu.

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

Koncové body programu

Nahrání programu

Program nahrajete v dashboardu Cloudflare v části Networking > L3/4 DDoS protection > Advanced Protection. Potom vyberte kartu Programmable Flow Protection.

V části Programy, klikněte na tlačítko "Upload new program." Zobrazí se výzva k výběru souboru, který nahrajete se svým C zdrojový kód.

Rozhraní Cloudflare API obdrží zdrojový kód v C souboru, zkompilujte jej do bajtkódu BPF a spusťte na něj verifikátor.

Pokud kompilace nebo ověření selže, API vrátí podrobnou chybovou zprávu.

Pokud kompilace a ověření proběhnou úspěšně, Cloudflare uloží k účtu zdrojový kód i objektový soubor a vrátí ID programu.

Aktualizace programu

Při vývoji se vám může hodit aktualizovat stále stejný program (identifikovaný stejným program ID) místo toho, abyste opakovaně vytvářeli nové programy jako nové zdroje.

Program aktualizujete kliknutím na tři tečky vedle něj. Poté vyberte Overwrite. Zobrazí se výzva k výběru souboru, který nahrajete jako C zdrojový kód.

Zobrazení všech programů

Všechny nahrané programy i stav jejich nasazení najdete v tabulce v sekci Programy.

Ikona odkazu vedle názvu programu značí, že program se právě používá v aktivním pravidle a nelze jej smazat.

Smazat program

Chcete-li program odstranit, vyberte tři tečky vedle programu, který chcete odstranit. Potom vyberte Smazat.

Program, na který odkazuje aktivní Rule, nelze smazat.

Programy se stavem "failed" (tedy ty, které se nepodařilo zkompilovat nebo neprošly ověřením) se po 30 dnech nečinnosti automaticky a trvale smažou.


Pravidla

Na každý paket se uplatní jen jedno pravidlo. Pokud máte v účtu nakonfigurováno více pravidel, použije se pravidlo s nejkonkrétnějším rozsah spustí. Například pravidlo s rozsahem konkrétního colo má přednost před pravidlem s rozsahem regionu a to má přednost před globálním pravidlem. Proto nemůžete vytvořit více než jedno globální pravidlo.

Výpis všech pravidel

Pravidla a jejich ID zobrazíte v Sítě > Ochrana před DDoS na vrstvě L3/4 > Advanced Protection v dashboardu Cloudflare. Poté vyberte Programmable Flow Protection.

Vytvoření pravidla

Chcete-li vytvořit pravidlo, přejděte do Sítě > Ochrana před DDoS na vrstvě L3/4 > Advanced Protection v dashboardu Cloudflare. Poté vyberte Programmable Flow Protection.

V části Pravidla, vyberte Vytvořit pravidlo. Vyplňte příslušná pole nového pravidla. Budete vyzváni k výběru programu, režimu a rozsahu pravidla.

Aktualizace pravidla

Existující pravidlo upravíte v sekci Rules. Klikněte na tři tečky vedle pravidla a vyberte Úprava.

Systém vás vyzve k úpravě režimu a rozsahu pravidla. Program pravidla upravit nelze, protože takový způsob zavádění změn není bezpečný.

Smazat pravidlo

Chcete-li odstranit existující pravidlo, přejděte do sekce Rules. Klikněte na tři tečky vedle pravidla a vyberte Smazat.


Debug Packet CAPture (PCAP)

Tento koncový bod API ladí program na základě těchto vstupů:

Tento koncový bod spustí odkazovaný program BPF nad vstupním souborem PCAP a vytvoří nový soubor PCAP s anotacemi. Výstupní soubor PCAP obsahuje přesně stejné pakety jako vstupní soubor a navíc verdikt programu zapsaný v Packet Comment sekci každého paketu.

Požadavek
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

Anotace Packet Comment může obsahovat:

Výstupní soubor PCAP může dále obsahovat:


Doporučené postupy pro bezpečné nasazení programů a pravidel

Programy budete chtít nasazovat a testovat bezpečně, bez dopadu na stávající produkční provoz. Při prvním nasazení se nabízí nastavit pravidlo s globálním rozsahem na disabled a nastavte pravidlo s rozsahem colo nebo region na monitoring s filtrovacím výrazem, který se vztahuje jen na určitou podmnožinu IP provozu.

Každý region nebo colo Cloudflare použije nejgranulárnější pravidlo. Ve výše popsaném scénáři tedy colo nebo regiony uvedené v monitoring pravidlo spustí vývojářský program v monitoring režimu, zatímco všechny ostatní lokality Cloudflare program vůbec nespustí. monitoring pravidlo by se spustilo pouze na provoz odpovídající výrazu filtru.

Poté, co si správné chování ověříte v Network Analytics, můžete aktualizovat a rozšířit monitoring pravidla rozsah a výraz filtru. Nakonec můžete smazat disabled a monitoring pravidla a použít globální enabled pravidlo.

Pomocí Expression k omezení programů na podmnožinu IP adres nebo prefixů a Mode k určení, zda program pakety skutečně zahazuje, zajišťuje bezpečné a jemně odstupňované zavádění programu.


Network Analytics

Provoz procházející přes Programmable Flow Protection najdete v Network Analytics dashboardu.

V dashboardu Network Analytics vyberte Programmable Flow Protection kartu použijte k filtrování provozu podle této funkce. Provoz lze filtrovat podle ID programu, vlastních tagů network analytics, akcí, IP adres a portů. Ve výchozím nastavení se pakety vzorkují v poměru 1/10,000.