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