Search code examples
linuxnetwork-programminganalyticsebpfxdp-bpf

How to create a graph of packets received vs packets allowed to pass


I have an XDP program where I am dropping every other packet received on the loopback device (will use a physical device in the future). I would like to create a graph of how many packets are received by the device (or the xdp program) vs how many packets were allowed to pass (XDP_PASS) using packets-per-second. My goal is to develop the program so that it mitigates a udp flood attack so I need to gather this type of data to measure its performance.


Solution

  • I will focus on the metrics transfer part from XDP to userspace since graphing the data itself a fairly large topic.

    If you only care about PASS/DROP overall, I can recommend basic03-map-count from xdp-tutorial.

    The final "assignment" in this tutorial is to convert the code to a per-CPU example. For DDoS related programs this is fairly critical since using shared maps will cause blocking. This is an example of such a program:

    #include <linux/bpf.h>
    
    #define SEC(NAME) __attribute__((section(NAME), used))
    
    #define XDP_MAX_ACTION 5
    
    // From https://github.com/libbpf/libbpf/blob/master/src/bpf_helper_defs.h
    static void *(*bpf_map_lookup_elem)(void *map, const void *key) = (void *) 1;
    
    struct bpf_map_def {
        unsigned int type;
        unsigned int key_size;
        unsigned int value_size;
        unsigned int max_entries;
        unsigned int map_flags;
    };
    
    struct datarec {
        __u64 rx_packets;
    };
    
    struct bpf_map_def SEC("maps") xdp_stats_map = {
        .type        = BPF_MAP_TYPE_PERCPU_ARRAY,
        .key_size    = sizeof(__u32),
        .value_size  = sizeof(struct datarec),
        .max_entries = XDP_MAX_ACTION,
    };
    
    SEC("xdp_stats1")
    int xdp_stats1_func(struct xdp_md *ctx)
    {
        // void *data_end = (void *)(long)ctx->data_end;
        // void *data     = (void *)(long)ctx->data;
        struct datarec *rec;
        __u32 action = XDP_PASS; /* XDP_PASS = 2 */
    
        // TODO add some logic, instread of returning directly, just set action to XDP_PASS or XDP_BLOCK
    
        /* Lookup in kernel BPF-side return pointer to actual data record */
        rec = bpf_map_lookup_elem(&xdp_stats_map, &action);
        if (!rec)
            return XDP_ABORTED;
    
        // Since xdp_stats_map is a per-CPU map, every logical-CPU/Core gets its own memory,
        //  we can safely increment without raceconditions or need for locking.
        rec->rx_packets++;
    
        return action;
    }
    
    char _license[] SEC("license") = "GPL";
    

    You will notice that we use the same map key, independent of time. This kind of program requires the userspace to poll the map at a 1 second interval and to calculate the diff. If you need 100% accurate stats or don't want to poll data each second you can include time in your key:

    #include <linux/bpf.h>
    
    #define SEC(NAME) __attribute__((section(NAME), used))
    
    #define XDP_MAX_ACTION 5
    
    // From https://github.com/libbpf/libbpf/blob/master/src/bpf_helper_defs.h
    static void *(*bpf_map_lookup_elem)(void *map, const void *key) = (void *) 1;
    
    static long (*bpf_map_update_elem)(void *map, const void *key, const void *value, __u64 flags) = (void *) 2;
    
    static __u64 (*bpf_ktime_get_ns)(void) = (void *) 5;
    
    struct bpf_map_def {
        unsigned int type;
        unsigned int key_size;
        unsigned int value_size;
        unsigned int max_entries;
        unsigned int map_flags;
    };
    
    struct timekey {
        __u32 action;
        __u32 second;
    };
    
    struct datarec {
        __u64 rx_packets;
        __u64 last_update;
    };
    
    struct bpf_map_def SEC("maps") xdp_stats_map = {
        .type        = BPF_MAP_TYPE_PERCPU_HASH,
        .key_size    = sizeof(struct timekey),
        .value_size  = sizeof(struct datarec),
        .max_entries = XDP_MAX_ACTION * 60,
    };
    
    #define SECOND_NS 1000000000
    
    SEC("xdp")
    int xdp_stats1_func(struct xdp_md *ctx)
    {
        // void *data_end = (void *)(long)ctx->data_end;
        // void *data     = (void *)(long)ctx->data;
        struct datarec *rec;
        struct timekey key;
        __u64 now;
    
        key.action = XDP_PASS; /* XDP_PASS = 2 */
    
        // TODO add some logic, instread of returning directly, just set action to XDP_PASS or XDP_BLOCK
    
        now = bpf_ktime_get_ns();
        key.second = (now / SECOND_NS) % 60;
    
        /* Lookup in kernel BPF-side return pointer to actual data record */
        rec = bpf_map_lookup_elem(&xdp_stats_map, &key);
        if (rec) {
            // If the last update to this key was more than 1 second ago, we are reusing the key, reset it.
            if (rec->last_update - now > SECOND_NS) {
                rec->rx_packets = 0;
            }
            rec->last_update = now;
            rec->rx_packets++;
        } else {
            struct datarec new_rec = {
                .rx_packets  = 1,
                .last_update = now,
            };
            bpf_map_update_elem(&xdp_stats_map, &key, &new_rec, BPF_ANY);
        }    
    
        return key.action;
    }
    
    char _license[] SEC("license") = "GPL";
    

    Also made a userspace example which shows how you might read the map from the second example. (sorry for the Go, my C skills don't go past simple eBPF programs):

    package main
    
    import (
        "bytes"
        "embed"
        "fmt"
        "os"
        "os/signal"
        "runtime"
        "time"
    
        "github.com/dylandreimerink/gobpfld"
        "github.com/dylandreimerink/gobpfld/bpftypes"
        "github.com/dylandreimerink/gobpfld/ebpf"
    )
    
    //go:embed src/xdp
    var f embed.FS
    
    func main() {
        elfFileBytes, err := f.ReadFile("src/xdp")
        if err != nil {
            fmt.Fprintf(os.Stderr, "error opening ELF file: %s\n", err.Error())
            os.Exit(1)
        }
    
        elf, err := gobpfld.LoadProgramFromELF(bytes.NewReader(elfFileBytes), gobpfld.ELFParseSettings{
            TruncateNames: true,
        })
        if err != nil {
            fmt.Fprintf(os.Stderr, "error while reading ELF file: %s\n", err.Error())
            os.Exit(1)
        }
    
        prog := elf.Programs["xdp_stats1_func"].(*gobpfld.ProgramXDP)
        log, err := prog.Load(gobpfld.ProgXDPLoadOpts{
            VerifierLogLevel: bpftypes.BPFLogLevelVerbose,
        })
        if err != nil {
            fmt.Println(log)
            fmt.Fprintf(os.Stderr, "error while loading progam: %s\n", err.Error())
            os.Exit(1)
        }
    
        err = prog.Attach(gobpfld.ProgXDPAttachOpts{
            InterfaceName: "lo",
        })
        if err != nil {
            fmt.Fprintf(os.Stderr, "error while loading progam: %s\n", err.Error())
            os.Exit(1)
        }
        defer func() {
            prog.XDPLinkDetach(gobpfld.BPFProgramXDPLinkDetachSettings{
                All: true,
            })
        }()
    
        statMap := prog.Maps["xdp_stats_map"].(*gobpfld.HashMap)
    
        sigChan := make(chan os.Signal, 1)
        signal.Notify(sigChan, os.Interrupt)
        ticker := time.NewTicker(1 * time.Second)
    
        done := false
        for !done {
            select {
            case <-ticker.C:
                var key MapKey
    
                // Since the map is a per-CPU type, the value we will read is an array with the same amount of elements
                // as logical CPU's
                value := make([]MapValue, runtime.NumCPU())
    
                // Map keyed by second, index keyed by action, value = count
                userMap := map[uint32][]uint32{}
    
                latest := uint64(0)
                latestSecond := int32(0)
    
                gobpfld.MapIterForEach(statMap.Iterator(), &key, &value, func(_, _ interface{}) error {
                    // Sum all values
                    total := make([]uint32, 5)
                    for _, val := range value {
                        total[key.Action] += uint32(val.PktCount)
    
                        // Record the latest changed key, this only works if we have at least 1 pkt/s.
                        if latest < val.LastUpdate {
                            latest = val.LastUpdate
                            latestSecond = int32(key.Second)
                        }
                    }
    
                    userMap[key.Second] = total
    
                    return nil
                })
    
                // We wan't the last second, not the current one, since it is still changing
                latestSecond--
                if latestSecond < 0 {
                    latestSecond += 60
                }
    
                values := userMap[uint32(latestSecond)]
                fmt.Printf("%02d: aborted: %d,  dropped: %d, passed: %d, tx'ed: %d, redirected: %d\n",
                    latestSecond,
                    values[ebpf.XDP_ABORTED],
                    values[ebpf.XDP_DROP],
                    values[ebpf.XDP_PASS],
                    values[ebpf.XDP_TX],
                    values[ebpf.XDP_REDIRECT],
                )
    
            case <-sigChan:
                done = true
            }
        }
    }
    
    type MapKey struct {
        Action uint32
        Second uint32
    }
    
    type MapValue struct {
        PktCount   uint64
        LastUpdate uint64
    }