-2

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.

rhoward
  • 131
  • 2
  • 3
  • 10
  • 1
    Please clarify, is your question "why do I see packets in wireshark/tcpdump while droping all packets in XDP"? Or about how to get metrics from XDP to userspace to create graphs? – Dylan Reimerink Jan 05 '22 at 17:49
  • My question is in the title. My comment about wireshark/tcpdump was just a comment about what I had tried. I removed it to avoid confusion. – rhoward Jan 05 '22 at 18:03

1 Answers1

1

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
}
Dylan Reimerink
  • 5,874
  • 2
  • 15
  • 21
  • I don't have a lot of C experience so correct me if I'm wrong. What is the purpose of `struct timekey`? it looks like you only assign to its 'second' field but we dont do anything with the struct or its fields. And why are we setting the size of the map key to `sizeof(struct timekey)` if we're still using `__32 action` as a key and not a `struct timekey`. – rhoward Jan 05 '22 at 20:35
  • 1
    Sorry, my mistake, I indeed forgot to change some things between the first and second example, I edited it. – Dylan Reimerink Jan 05 '22 at 20:40