root/tools/testing/selftests/bpf/xdp_features.c
// SPDX-License-Identifier: GPL-2.0
#include <uapi/linux/bpf.h>
#include <uapi/linux/netdev.h>
#include <linux/if_link.h>
#include <signal.h>
#include <argp.h>
#include <net/if.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <bpf/bpf.h>
#include <bpf/libbpf.h>
#include <pthread.h>

#include <network_helpers.h>

#include "bpf_util.h"
#include "xdp_features.skel.h"
#include "xdp_features.h"

#define RED(str)        "\033[0;31m" str "\033[0m"
#define GREEN(str)      "\033[0;32m" str "\033[0m"
#define YELLOW(str)     "\033[0;33m" str "\033[0m"

static struct env {
        bool verbosity;
        char ifname[IF_NAMESIZE];
        int ifindex;
        bool is_tester;
        struct {
                enum netdev_xdp_act drv_feature;
                enum xdp_action action;
        } feature;
        struct sockaddr_storage dut_ctrl_addr;
        struct sockaddr_storage dut_addr;
        struct sockaddr_storage tester_addr;
} env;

#define BUFSIZE         128

void test__fail(void) { /* for network_helpers.c */ }

static int libbpf_print_fn(enum libbpf_print_level level,
                           const char *format, va_list args)
{
        if (level == LIBBPF_DEBUG && !env.verbosity)
                return 0;
        return vfprintf(stderr, format, args);
}

static volatile bool exiting;

static void sig_handler(int sig)
{
        exiting = true;
}

const char *argp_program_version = "xdp-features 0.0";
const char argp_program_doc[] =
"XDP features detection application.\n"
"\n"
"XDP features application checks the XDP advertised features match detected ones.\n"
"\n"
"USAGE: ./xdp-features [-vt] [-f <xdp-feature>] [-D <dut-data-ip>] [-T <tester-data-ip>] [-C <dut-ctrl-ip>] <iface-name>\n"
"\n"
"dut-data-ip, tester-data-ip, dut-ctrl-ip: IPv6 or IPv4-mapped-IPv6 addresses;\n"
"\n"
"XDP features\n:"
"- XDP_PASS\n"
"- XDP_DROP\n"
"- XDP_ABORTED\n"
"- XDP_REDIRECT\n"
"- XDP_NDO_XMIT\n"
"- XDP_TX\n";

static const struct argp_option opts[] = {
        { "verbose", 'v', NULL, 0, "Verbose debug output" },
        { "tester", 't', NULL, 0, "Tester mode" },
        { "feature", 'f', "XDP-FEATURE", 0, "XDP feature to test" },
        { "dut_data_ip", 'D', "DUT-DATA-IP", 0, "DUT IP data channel" },
        { "dut_ctrl_ip", 'C', "DUT-CTRL-IP", 0, "DUT IP control channel" },
        { "tester_data_ip", 'T', "TESTER-DATA-IP", 0, "Tester IP data channel" },
        {},
};

static int get_xdp_feature(const char *arg)
{
        if (!strcmp(arg, "XDP_PASS")) {
                env.feature.action = XDP_PASS;
                env.feature.drv_feature = NETDEV_XDP_ACT_BASIC;
        } else if (!strcmp(arg, "XDP_DROP")) {
                env.feature.drv_feature = NETDEV_XDP_ACT_BASIC;
                env.feature.action = XDP_DROP;
        } else if (!strcmp(arg, "XDP_ABORTED")) {
                env.feature.drv_feature = NETDEV_XDP_ACT_BASIC;
                env.feature.action = XDP_ABORTED;
        } else if (!strcmp(arg, "XDP_TX")) {
                env.feature.drv_feature = NETDEV_XDP_ACT_BASIC;
                env.feature.action = XDP_TX;
        } else if (!strcmp(arg, "XDP_REDIRECT")) {
                env.feature.drv_feature = NETDEV_XDP_ACT_REDIRECT;
                env.feature.action = XDP_REDIRECT;
        } else if (!strcmp(arg, "XDP_NDO_XMIT")) {
                env.feature.drv_feature = NETDEV_XDP_ACT_NDO_XMIT;
        } else {
                return -EINVAL;
        }

        return 0;
}

static char *get_xdp_feature_str(void)
{
        switch (env.feature.action) {
        case XDP_PASS:
                return YELLOW("XDP_PASS");
        case XDP_DROP:
                return YELLOW("XDP_DROP");
        case XDP_ABORTED:
                return YELLOW("XDP_ABORTED");
        case XDP_TX:
                return YELLOW("XDP_TX");
        case XDP_REDIRECT:
                return YELLOW("XDP_REDIRECT");
        default:
                break;
        }

        if (env.feature.drv_feature == NETDEV_XDP_ACT_NDO_XMIT)
                return YELLOW("XDP_NDO_XMIT");

        return "";
}

static error_t parse_arg(int key, char *arg, struct argp_state *state)
{
        switch (key) {
        case 'v':
                env.verbosity = true;
                break;
        case 't':
                env.is_tester = true;
                break;
        case 'f':
                if (get_xdp_feature(arg) < 0) {
                        fprintf(stderr, "Invalid xdp feature: %s\n", arg);
                        argp_usage(state);
                        return ARGP_ERR_UNKNOWN;
                }
                break;
        case 'D':
                if (make_sockaddr(AF_INET6, arg, DUT_ECHO_PORT,
                                  &env.dut_addr, NULL)) {
                        fprintf(stderr,
                                "Invalid address assigned to the Device Under Test: %s\n",
                                arg);
                        return ARGP_ERR_UNKNOWN;
                }
                break;
        case 'C':
                if (make_sockaddr(AF_INET6, arg, DUT_CTRL_PORT,
                                  &env.dut_ctrl_addr, NULL)) {
                        fprintf(stderr,
                                "Invalid address assigned to the Device Under Test: %s\n",
                                arg);
                        return ARGP_ERR_UNKNOWN;
                }
                break;
        case 'T':
                if (make_sockaddr(AF_INET6, arg, 0, &env.tester_addr, NULL)) {
                        fprintf(stderr,
                                "Invalid address assigned to the Tester device: %s\n",
                                arg);
                        return ARGP_ERR_UNKNOWN;
                }
                break;
        case ARGP_KEY_ARG:
                errno = 0;
                if (strlen(arg) >= IF_NAMESIZE) {
                        fprintf(stderr, "Invalid device name: %s\n", arg);
                        argp_usage(state);
                        return ARGP_ERR_UNKNOWN;
                }

                env.ifindex = if_nametoindex(arg);
                if (!env.ifindex)
                        env.ifindex = strtoul(arg, NULL, 0);
                if (!env.ifindex || !if_indextoname(env.ifindex, env.ifname)) {
                        fprintf(stderr,
                                "Bad interface index or name (%d): %s\n",
                                errno, strerror(errno));
                        argp_usage(state);
                        return ARGP_ERR_UNKNOWN;
                }
                break;
        default:
                return ARGP_ERR_UNKNOWN;
        }

        return 0;
}

static const struct argp argp = {
        .options = opts,
        .parser = parse_arg,
        .doc = argp_program_doc,
};

static void set_env_default(void)
{
        env.feature.drv_feature = NETDEV_XDP_ACT_NDO_XMIT;
        env.feature.action = -EINVAL;
        env.ifindex = -ENODEV;
        strscpy(env.ifname, "unknown");
        make_sockaddr(AF_INET6, "::ffff:127.0.0.1", DUT_CTRL_PORT,
                      &env.dut_ctrl_addr, NULL);
        make_sockaddr(AF_INET6, "::ffff:127.0.0.1", DUT_ECHO_PORT,
                      &env.dut_addr, NULL);
        make_sockaddr(AF_INET6, "::ffff:127.0.0.1", 0, &env.tester_addr, NULL);
}

static void *dut_echo_thread(void *arg)
{
        unsigned char buf[sizeof(struct tlv_hdr)];
        int sockfd = *(int *)arg;

        while (!exiting) {
                struct tlv_hdr *tlv = (struct tlv_hdr *)buf;
                struct sockaddr_storage addr;
                socklen_t addrlen;
                size_t n;

                n = recvfrom(sockfd, buf, sizeof(buf), MSG_WAITALL,
                             (struct sockaddr *)&addr, &addrlen);
                if (n != ntohs(tlv->len))
                        continue;

                if (ntohs(tlv->type) != CMD_ECHO)
                        continue;

                sendto(sockfd, buf, sizeof(buf), MSG_NOSIGNAL | MSG_CONFIRM,
                       (struct sockaddr *)&addr, addrlen);
        }

        pthread_exit((void *)0);
        close(sockfd);

        return NULL;
}

static int dut_run_echo_thread(pthread_t *t, int *sockfd)
{
        int err;

        sockfd = start_reuseport_server(AF_INET6, SOCK_DGRAM, NULL,
                                        DUT_ECHO_PORT, 0, 1);
        if (!sockfd) {
                fprintf(stderr,
                        "Failed creating data UDP socket on device %s\n",
                        env.ifname);
                return -errno;
        }

        /* start echo channel */
        err = pthread_create(t, NULL, dut_echo_thread, sockfd);
        if (err) {
                fprintf(stderr,
                        "Failed creating data UDP thread on device %s: %s\n",
                        env.ifname, strerror(-err));
                free_fds(sockfd, 1);
                return -EINVAL;
        }

        return 0;
}

static int dut_attach_xdp_prog(struct xdp_features *skel, int flags)
{
        enum xdp_action action = env.feature.action;
        struct bpf_program *prog;
        unsigned int key = 0;
        int err, fd = 0;

        if (env.feature.drv_feature == NETDEV_XDP_ACT_NDO_XMIT) {
                struct bpf_devmap_val entry = {
                        .ifindex = env.ifindex,
                };

                err = bpf_map__update_elem(skel->maps.dev_map,
                                           &key, sizeof(key),
                                           &entry, sizeof(entry), 0);
                if (err < 0)
                        return err;

                fd = bpf_program__fd(skel->progs.xdp_do_redirect_cpumap);
                action = XDP_REDIRECT;
        }

        switch (action) {
        case XDP_TX:
                prog = skel->progs.xdp_do_tx;
                break;
        case XDP_DROP:
                prog = skel->progs.xdp_do_drop;
                break;
        case XDP_ABORTED:
                prog = skel->progs.xdp_do_aborted;
                break;
        case XDP_PASS:
                prog = skel->progs.xdp_do_pass;
                break;
        case XDP_REDIRECT: {
                struct bpf_cpumap_val entry = {
                        .qsize = 2048,
                        .bpf_prog.fd = fd,
                };

                err = bpf_map__update_elem(skel->maps.cpu_map,
                                           &key, sizeof(key),
                                           &entry, sizeof(entry), 0);
                if (err < 0)
                        return err;

                prog = skel->progs.xdp_do_redirect;
                break;
        }
        default:
                return -EINVAL;
        }

        err = bpf_xdp_attach(env.ifindex, bpf_program__fd(prog), flags, NULL);
        if (err)
                fprintf(stderr, "Failed attaching XDP program to device %s\n",
                        env.ifname);
        return err;
}

static int recv_msg(int sockfd, void *buf, size_t bufsize, void *val,
                    size_t val_size)
{
        struct tlv_hdr *tlv = (struct tlv_hdr *)buf;
        size_t len;

        len = recv(sockfd, buf, bufsize, 0);
        if (len != ntohs(tlv->len) || len < sizeof(*tlv))
                return -EINVAL;

        if (val) {
                len -= sizeof(*tlv);
                if (len > val_size)
                        return -ENOMEM;

                memcpy(val, tlv->data, len);
        }

        return 0;
}

static int dut_run(struct xdp_features *skel)
{
        int flags = XDP_FLAGS_UPDATE_IF_NOEXIST | XDP_FLAGS_DRV_MODE;
        int state, err = 0, *sockfd, ctrl_sockfd, echo_sockfd;
        struct sockaddr_storage ctrl_addr;
        pthread_t dut_thread = 0;
        socklen_t addrlen;

        sockfd = start_reuseport_server(AF_INET6, SOCK_STREAM, NULL,
                                        DUT_CTRL_PORT, 0, 1);
        if (!sockfd) {
                fprintf(stderr,
                        "Failed creating control socket on device %s\n", env.ifname);
                return -errno;
        }

        ctrl_sockfd = accept(*sockfd, (struct sockaddr *)&ctrl_addr, &addrlen);
        if (ctrl_sockfd < 0) {
                fprintf(stderr,
                        "Failed accepting connections on device %s control socket\n",
                        env.ifname);
                free_fds(sockfd, 1);
                return -errno;
        }

        /* CTRL loop */
        while (!exiting) {
                unsigned char buf[BUFSIZE] = {};
                struct tlv_hdr *tlv = (struct tlv_hdr *)buf;

                err = recv_msg(ctrl_sockfd, buf, BUFSIZE, NULL, 0);
                if (err)
                        continue;

                switch (ntohs(tlv->type)) {
                case CMD_START: {
                        if (state == CMD_START)
                                continue;

                        state = CMD_START;
                        /* Load the XDP program on the DUT */
                        err = dut_attach_xdp_prog(skel, flags);
                        if (err)
                                goto out;

                        err = dut_run_echo_thread(&dut_thread, &echo_sockfd);
                        if (err < 0)
                                goto out;

                        tlv->type = htons(CMD_ACK);
                        tlv->len = htons(sizeof(*tlv));
                        err = send(ctrl_sockfd, buf, sizeof(*tlv), 0);
                        if (err < 0)
                                goto end_thread;
                        break;
                }
                case CMD_STOP:
                        if (state != CMD_START)
                                break;

                        state = CMD_STOP;

                        exiting = true;
                        bpf_xdp_detach(env.ifindex, flags, NULL);

                        tlv->type = htons(CMD_ACK);
                        tlv->len = htons(sizeof(*tlv));
                        err = send(ctrl_sockfd, buf, sizeof(*tlv), 0);
                        goto end_thread;
                case CMD_GET_XDP_CAP: {
                        LIBBPF_OPTS(bpf_xdp_query_opts, opts);
                        unsigned long long val;
                        size_t n;

                        err = bpf_xdp_query(env.ifindex, XDP_FLAGS_DRV_MODE,
                                            &opts);
                        if (err) {
                                fprintf(stderr,
                                        "Failed querying XDP cap for device %s\n",
                                        env.ifname);
                                goto end_thread;
                        }

                        tlv->type = htons(CMD_ACK);
                        n = sizeof(*tlv) + sizeof(opts.feature_flags);
                        tlv->len = htons(n);

                        val = htobe64(opts.feature_flags);
                        memcpy(tlv->data, &val, sizeof(val));

                        err = send(ctrl_sockfd, buf, n, 0);
                        if (err < 0)
                                goto end_thread;
                        break;
                }
                case CMD_GET_STATS: {
                        unsigned int key = 0, val;
                        size_t n;

                        err = bpf_map__lookup_elem(skel->maps.dut_stats,
                                                   &key, sizeof(key),
                                                   &val, sizeof(val), 0);
                        if (err) {
                                fprintf(stderr,
                                        "bpf_map_lookup_elem failed (%d)\n", err);
                                goto end_thread;
                        }

                        tlv->type = htons(CMD_ACK);
                        n = sizeof(*tlv) + sizeof(val);
                        tlv->len = htons(n);

                        val = htonl(val);
                        memcpy(tlv->data, &val, sizeof(val));

                        err = send(ctrl_sockfd, buf, n, 0);
                        if (err < 0)
                                goto end_thread;
                        break;
                }
                default:
                        break;
                }
        }

end_thread:
        pthread_join(dut_thread, NULL);
out:
        bpf_xdp_detach(env.ifindex, flags, NULL);
        close(ctrl_sockfd);
        free_fds(sockfd, 1);

        return err;
}

static bool tester_collect_detected_cap(struct xdp_features *skel,
                                        unsigned int dut_stats)
{
        unsigned int err, key = 0, val;

        if (!dut_stats)
                return false;

        err = bpf_map__lookup_elem(skel->maps.stats, &key, sizeof(key),
                                   &val, sizeof(val), 0);
        if (err) {
                fprintf(stderr, "bpf_map_lookup_elem failed (%d)\n", err);
                return false;
        }

        switch (env.feature.action) {
        case XDP_PASS:
        case XDP_TX:
        case XDP_REDIRECT:
                return val > 0;
        case XDP_DROP:
        case XDP_ABORTED:
                return val == 0;
        default:
                break;
        }

        if (env.feature.drv_feature == NETDEV_XDP_ACT_NDO_XMIT)
                return val > 0;

        return false;
}

static int send_and_recv_msg(int sockfd, enum test_commands cmd, void *val,
                             size_t val_size)
{
        unsigned char buf[BUFSIZE] = {};
        struct tlv_hdr *tlv = (struct tlv_hdr *)buf;
        int err;

        tlv->type = htons(cmd);
        tlv->len = htons(sizeof(*tlv));

        err = send(sockfd, buf, sizeof(*tlv), 0);
        if (err < 0)
                return err;

        err = recv_msg(sockfd, buf, BUFSIZE, val, val_size);
        if (err < 0)
                return err;

        return ntohs(tlv->type) == CMD_ACK ? 0 : -EINVAL;
}

static int send_echo_msg(void)
{
        unsigned char buf[sizeof(struct tlv_hdr)];
        struct tlv_hdr *tlv = (struct tlv_hdr *)buf;
        int sockfd, n;

        sockfd = socket(AF_INET6, SOCK_DGRAM, 0);
        if (sockfd < 0) {
                fprintf(stderr,
                        "Failed creating data UDP socket on device %s\n",
                        env.ifname);
                return -errno;
        }

        tlv->type = htons(CMD_ECHO);
        tlv->len = htons(sizeof(*tlv));

        n = sendto(sockfd, buf, sizeof(*tlv), MSG_NOSIGNAL | MSG_CONFIRM,
                   (struct sockaddr *)&env.dut_addr, sizeof(env.dut_addr));
        close(sockfd);

        return n == ntohs(tlv->len) ? 0 : -EINVAL;
}

static int tester_run(struct xdp_features *skel)
{
        int flags = XDP_FLAGS_UPDATE_IF_NOEXIST | XDP_FLAGS_DRV_MODE;
        unsigned long long advertised_feature;
        struct bpf_program *prog;
        unsigned int stats;
        int i, err, sockfd;
        bool detected_cap;

        sockfd = socket(AF_INET6, SOCK_STREAM, 0);
        if (sockfd < 0) {
                fprintf(stderr,
                        "Failed creating tester service control socket\n");
                return -errno;
        }

        if (settimeo(sockfd, 1000) < 0)
                return -EINVAL;

        err = connect(sockfd, (struct sockaddr *)&env.dut_ctrl_addr,
                      sizeof(env.dut_ctrl_addr));
        if (err) {
                fprintf(stderr,
                        "Failed connecting to the Device Under Test control socket\n");
                return -errno;
        }

        err = send_and_recv_msg(sockfd, CMD_GET_XDP_CAP, &advertised_feature,
                                sizeof(advertised_feature));
        if (err < 0) {
                close(sockfd);
                return err;
        }

        advertised_feature = be64toh(advertised_feature);

        if (env.feature.drv_feature == NETDEV_XDP_ACT_NDO_XMIT ||
            env.feature.action == XDP_TX)
                prog = skel->progs.xdp_tester_check_tx;
        else
                prog = skel->progs.xdp_tester_check_rx;

        err = bpf_xdp_attach(env.ifindex, bpf_program__fd(prog), flags, NULL);
        if (err) {
                fprintf(stderr, "Failed attaching XDP program to device %s\n",
                        env.ifname);
                goto out;
        }

        err = send_and_recv_msg(sockfd, CMD_START, NULL, 0);
        if (err)
                goto out;

        for (i = 0; i < 10 && !exiting; i++) {
                err = send_echo_msg();
                if (err < 0)
                        goto out;

                sleep(1);
        }

        err = send_and_recv_msg(sockfd, CMD_GET_STATS, &stats, sizeof(stats));
        if (err)
                goto out;

        /* stop the test */
        err = send_and_recv_msg(sockfd, CMD_STOP, NULL, 0);
        /* send a new echo message to wake echo thread of the dut */
        send_echo_msg();

        detected_cap = tester_collect_detected_cap(skel, ntohl(stats));

        fprintf(stdout, "Feature %s: [%s][%s]\n", get_xdp_feature_str(),
                detected_cap ? GREEN("DETECTED") : RED("NOT DETECTED"),
                env.feature.drv_feature & advertised_feature ? GREEN("ADVERTISED")
                                                             : RED("NOT ADVERTISED"));
out:
        bpf_xdp_detach(env.ifindex, flags, NULL);
        close(sockfd);
        return err < 0 ? err : 0;
}

int main(int argc, char **argv)
{
        struct xdp_features *skel;
        int err;

        libbpf_set_strict_mode(LIBBPF_STRICT_ALL);
        libbpf_set_print(libbpf_print_fn);

        signal(SIGINT, sig_handler);
        signal(SIGTERM, sig_handler);

        set_env_default();

        /* Parse command line arguments */
        err = argp_parse(&argp, argc, argv, 0, NULL, NULL);
        if (err)
                return err;

        if (env.ifindex < 0) {
                fprintf(stderr, "Invalid device name %s\n", env.ifname);
                return -ENODEV;
        }

        /* Load and verify BPF application */
        skel = xdp_features__open();
        if (!skel) {
                fprintf(stderr, "Failed to open and load BPF skeleton\n");
                return -EINVAL;
        }

        skel->rodata->tester_addr =
                ((struct sockaddr_in6 *)&env.tester_addr)->sin6_addr;
        skel->rodata->dut_addr =
                ((struct sockaddr_in6 *)&env.dut_addr)->sin6_addr;

        /* Load & verify BPF programs */
        err = xdp_features__load(skel);
        if (err) {
                fprintf(stderr, "Failed to load and verify BPF skeleton\n");
                goto cleanup;
        }

        err = xdp_features__attach(skel);
        if (err) {
                fprintf(stderr, "Failed to attach BPF skeleton\n");
                goto cleanup;
        }

        if (env.is_tester) {
                /* Tester */
                fprintf(stdout, "Starting tester service on device %s\n",
                        env.ifname);
                err = tester_run(skel);
        } else {
                /* DUT */
                fprintf(stdout, "Starting test on device %s\n", env.ifname);
                err = dut_run(skel);
        }

cleanup:
        xdp_features__destroy(skel);

        return err < 0 ? -err : 0;
}