1 // SPDX-License-Identifier: GPL-2.0
2 #include <uapi/linux/bpf.h>
3 #include <uapi/linux/netdev.h>
4 #include <linux/if_link.h>
5 #include <signal.h>
6 #include <argp.h>
7 #include <net/if.h>
8 #include <sys/socket.h>
9 #include <netinet/in.h>
10 #include <netinet/tcp.h>
11 #include <unistd.h>
12 #include <arpa/inet.h>
13 #include <bpf/bpf.h>
14 #include <bpf/libbpf.h>
15 #include <pthread.h>
16
17 #include <network_helpers.h>
18
19 #include "bpf_util.h"
20 #include "xdp_features.skel.h"
21 #include "xdp_features.h"
22
23 #define RED(str) "\033[0;31m" str "\033[0m"
24 #define GREEN(str) "\033[0;32m" str "\033[0m"
25 #define YELLOW(str) "\033[0;33m" str "\033[0m"
26
27 static struct env {
28 bool verbosity;
29 char ifname[IF_NAMESIZE];
30 int ifindex;
31 bool is_tester;
32 struct {
33 enum netdev_xdp_act drv_feature;
34 enum xdp_action action;
35 } feature;
36 struct sockaddr_storage dut_ctrl_addr;
37 struct sockaddr_storage dut_addr;
38 struct sockaddr_storage tester_addr;
39 } env;
40
41 #define BUFSIZE 128
42
test__fail(void)43 void test__fail(void) { /* for network_helpers.c */ }
44
libbpf_print_fn(enum libbpf_print_level level,const char * format,va_list args)45 static int libbpf_print_fn(enum libbpf_print_level level,
46 const char *format, va_list args)
47 {
48 if (level == LIBBPF_DEBUG && !env.verbosity)
49 return 0;
50 return vfprintf(stderr, format, args);
51 }
52
53 static volatile bool exiting;
54
sig_handler(int sig)55 static void sig_handler(int sig)
56 {
57 exiting = true;
58 }
59
60 const char *argp_program_version = "xdp-features 0.0";
61 const char argp_program_doc[] =
62 "XDP features detection application.\n"
63 "\n"
64 "XDP features application checks the XDP advertised features match detected ones.\n"
65 "\n"
66 "USAGE: ./xdp-features [-vt] [-f <xdp-feature>] [-D <dut-data-ip>] [-T <tester-data-ip>] [-C <dut-ctrl-ip>] <iface-name>\n"
67 "\n"
68 "dut-data-ip, tester-data-ip, dut-ctrl-ip: IPv6 or IPv4-mapped-IPv6 addresses;\n"
69 "\n"
70 "XDP features\n:"
71 "- XDP_PASS\n"
72 "- XDP_DROP\n"
73 "- XDP_ABORTED\n"
74 "- XDP_REDIRECT\n"
75 "- XDP_NDO_XMIT\n"
76 "- XDP_TX\n";
77
78 static const struct argp_option opts[] = {
79 { "verbose", 'v', NULL, 0, "Verbose debug output" },
80 { "tester", 't', NULL, 0, "Tester mode" },
81 { "feature", 'f', "XDP-FEATURE", 0, "XDP feature to test" },
82 { "dut_data_ip", 'D', "DUT-DATA-IP", 0, "DUT IP data channel" },
83 { "dut_ctrl_ip", 'C', "DUT-CTRL-IP", 0, "DUT IP control channel" },
84 { "tester_data_ip", 'T', "TESTER-DATA-IP", 0, "Tester IP data channel" },
85 {},
86 };
87
get_xdp_feature(const char * arg)88 static int get_xdp_feature(const char *arg)
89 {
90 if (!strcmp(arg, "XDP_PASS")) {
91 env.feature.action = XDP_PASS;
92 env.feature.drv_feature = NETDEV_XDP_ACT_BASIC;
93 } else if (!strcmp(arg, "XDP_DROP")) {
94 env.feature.drv_feature = NETDEV_XDP_ACT_BASIC;
95 env.feature.action = XDP_DROP;
96 } else if (!strcmp(arg, "XDP_ABORTED")) {
97 env.feature.drv_feature = NETDEV_XDP_ACT_BASIC;
98 env.feature.action = XDP_ABORTED;
99 } else if (!strcmp(arg, "XDP_TX")) {
100 env.feature.drv_feature = NETDEV_XDP_ACT_BASIC;
101 env.feature.action = XDP_TX;
102 } else if (!strcmp(arg, "XDP_REDIRECT")) {
103 env.feature.drv_feature = NETDEV_XDP_ACT_REDIRECT;
104 env.feature.action = XDP_REDIRECT;
105 } else if (!strcmp(arg, "XDP_NDO_XMIT")) {
106 env.feature.drv_feature = NETDEV_XDP_ACT_NDO_XMIT;
107 } else {
108 return -EINVAL;
109 }
110
111 return 0;
112 }
113
get_xdp_feature_str(void)114 static char *get_xdp_feature_str(void)
115 {
116 switch (env.feature.action) {
117 case XDP_PASS:
118 return YELLOW("XDP_PASS");
119 case XDP_DROP:
120 return YELLOW("XDP_DROP");
121 case XDP_ABORTED:
122 return YELLOW("XDP_ABORTED");
123 case XDP_TX:
124 return YELLOW("XDP_TX");
125 case XDP_REDIRECT:
126 return YELLOW("XDP_REDIRECT");
127 default:
128 break;
129 }
130
131 if (env.feature.drv_feature == NETDEV_XDP_ACT_NDO_XMIT)
132 return YELLOW("XDP_NDO_XMIT");
133
134 return "";
135 }
136
parse_arg(int key,char * arg,struct argp_state * state)137 static error_t parse_arg(int key, char *arg, struct argp_state *state)
138 {
139 switch (key) {
140 case 'v':
141 env.verbosity = true;
142 break;
143 case 't':
144 env.is_tester = true;
145 break;
146 case 'f':
147 if (get_xdp_feature(arg) < 0) {
148 fprintf(stderr, "Invalid xdp feature: %s\n", arg);
149 argp_usage(state);
150 return ARGP_ERR_UNKNOWN;
151 }
152 break;
153 case 'D':
154 if (make_sockaddr(AF_INET6, arg, DUT_ECHO_PORT,
155 &env.dut_addr, NULL)) {
156 fprintf(stderr,
157 "Invalid address assigned to the Device Under Test: %s\n",
158 arg);
159 return ARGP_ERR_UNKNOWN;
160 }
161 break;
162 case 'C':
163 if (make_sockaddr(AF_INET6, arg, DUT_CTRL_PORT,
164 &env.dut_ctrl_addr, NULL)) {
165 fprintf(stderr,
166 "Invalid address assigned to the Device Under Test: %s\n",
167 arg);
168 return ARGP_ERR_UNKNOWN;
169 }
170 break;
171 case 'T':
172 if (make_sockaddr(AF_INET6, arg, 0, &env.tester_addr, NULL)) {
173 fprintf(stderr,
174 "Invalid address assigned to the Tester device: %s\n",
175 arg);
176 return ARGP_ERR_UNKNOWN;
177 }
178 break;
179 case ARGP_KEY_ARG:
180 errno = 0;
181 if (strlen(arg) >= IF_NAMESIZE) {
182 fprintf(stderr, "Invalid device name: %s\n", arg);
183 argp_usage(state);
184 return ARGP_ERR_UNKNOWN;
185 }
186
187 env.ifindex = if_nametoindex(arg);
188 if (!env.ifindex)
189 env.ifindex = strtoul(arg, NULL, 0);
190 if (!env.ifindex || !if_indextoname(env.ifindex, env.ifname)) {
191 fprintf(stderr,
192 "Bad interface index or name (%d): %s\n",
193 errno, strerror(errno));
194 argp_usage(state);
195 return ARGP_ERR_UNKNOWN;
196 }
197 break;
198 default:
199 return ARGP_ERR_UNKNOWN;
200 }
201
202 return 0;
203 }
204
205 static const struct argp argp = {
206 .options = opts,
207 .parser = parse_arg,
208 .doc = argp_program_doc,
209 };
210
set_env_default(void)211 static void set_env_default(void)
212 {
213 env.feature.drv_feature = NETDEV_XDP_ACT_NDO_XMIT;
214 env.feature.action = -EINVAL;
215 env.ifindex = -ENODEV;
216 strscpy(env.ifname, "unknown");
217 make_sockaddr(AF_INET6, "::ffff:127.0.0.1", DUT_CTRL_PORT,
218 &env.dut_ctrl_addr, NULL);
219 make_sockaddr(AF_INET6, "::ffff:127.0.0.1", DUT_ECHO_PORT,
220 &env.dut_addr, NULL);
221 make_sockaddr(AF_INET6, "::ffff:127.0.0.1", 0, &env.tester_addr, NULL);
222 }
223
dut_echo_thread(void * arg)224 static void *dut_echo_thread(void *arg)
225 {
226 unsigned char buf[sizeof(struct tlv_hdr)];
227 int sockfd = *(int *)arg;
228
229 while (!exiting) {
230 struct tlv_hdr *tlv = (struct tlv_hdr *)buf;
231 struct sockaddr_storage addr;
232 socklen_t addrlen;
233 size_t n;
234
235 n = recvfrom(sockfd, buf, sizeof(buf), MSG_WAITALL,
236 (struct sockaddr *)&addr, &addrlen);
237 if (n != ntohs(tlv->len))
238 continue;
239
240 if (ntohs(tlv->type) != CMD_ECHO)
241 continue;
242
243 sendto(sockfd, buf, sizeof(buf), MSG_NOSIGNAL | MSG_CONFIRM,
244 (struct sockaddr *)&addr, addrlen);
245 }
246
247 pthread_exit((void *)0);
248 close(sockfd);
249
250 return NULL;
251 }
252
dut_run_echo_thread(pthread_t * t,int * sockfd)253 static int dut_run_echo_thread(pthread_t *t, int *sockfd)
254 {
255 int err;
256
257 sockfd = start_reuseport_server(AF_INET6, SOCK_DGRAM, NULL,
258 DUT_ECHO_PORT, 0, 1);
259 if (!sockfd) {
260 fprintf(stderr,
261 "Failed creating data UDP socket on device %s\n",
262 env.ifname);
263 return -errno;
264 }
265
266 /* start echo channel */
267 err = pthread_create(t, NULL, dut_echo_thread, sockfd);
268 if (err) {
269 fprintf(stderr,
270 "Failed creating data UDP thread on device %s: %s\n",
271 env.ifname, strerror(-err));
272 free_fds(sockfd, 1);
273 return -EINVAL;
274 }
275
276 return 0;
277 }
278
dut_attach_xdp_prog(struct xdp_features * skel,int flags)279 static int dut_attach_xdp_prog(struct xdp_features *skel, int flags)
280 {
281 enum xdp_action action = env.feature.action;
282 struct bpf_program *prog;
283 unsigned int key = 0;
284 int err, fd = 0;
285
286 if (env.feature.drv_feature == NETDEV_XDP_ACT_NDO_XMIT) {
287 struct bpf_devmap_val entry = {
288 .ifindex = env.ifindex,
289 };
290
291 err = bpf_map__update_elem(skel->maps.dev_map,
292 &key, sizeof(key),
293 &entry, sizeof(entry), 0);
294 if (err < 0)
295 return err;
296
297 fd = bpf_program__fd(skel->progs.xdp_do_redirect_cpumap);
298 action = XDP_REDIRECT;
299 }
300
301 switch (action) {
302 case XDP_TX:
303 prog = skel->progs.xdp_do_tx;
304 break;
305 case XDP_DROP:
306 prog = skel->progs.xdp_do_drop;
307 break;
308 case XDP_ABORTED:
309 prog = skel->progs.xdp_do_aborted;
310 break;
311 case XDP_PASS:
312 prog = skel->progs.xdp_do_pass;
313 break;
314 case XDP_REDIRECT: {
315 struct bpf_cpumap_val entry = {
316 .qsize = 2048,
317 .bpf_prog.fd = fd,
318 };
319
320 err = bpf_map__update_elem(skel->maps.cpu_map,
321 &key, sizeof(key),
322 &entry, sizeof(entry), 0);
323 if (err < 0)
324 return err;
325
326 prog = skel->progs.xdp_do_redirect;
327 break;
328 }
329 default:
330 return -EINVAL;
331 }
332
333 err = bpf_xdp_attach(env.ifindex, bpf_program__fd(prog), flags, NULL);
334 if (err)
335 fprintf(stderr, "Failed attaching XDP program to device %s\n",
336 env.ifname);
337 return err;
338 }
339
recv_msg(int sockfd,void * buf,size_t bufsize,void * val,size_t val_size)340 static int recv_msg(int sockfd, void *buf, size_t bufsize, void *val,
341 size_t val_size)
342 {
343 struct tlv_hdr *tlv = (struct tlv_hdr *)buf;
344 size_t len;
345
346 len = recv(sockfd, buf, bufsize, 0);
347 if (len != ntohs(tlv->len) || len < sizeof(*tlv))
348 return -EINVAL;
349
350 if (val) {
351 len -= sizeof(*tlv);
352 if (len > val_size)
353 return -ENOMEM;
354
355 memcpy(val, tlv->data, len);
356 }
357
358 return 0;
359 }
360
dut_run(struct xdp_features * skel)361 static int dut_run(struct xdp_features *skel)
362 {
363 int flags = XDP_FLAGS_UPDATE_IF_NOEXIST | XDP_FLAGS_DRV_MODE;
364 int state, err = 0, *sockfd, ctrl_sockfd, echo_sockfd;
365 struct sockaddr_storage ctrl_addr;
366 pthread_t dut_thread = 0;
367 socklen_t addrlen;
368
369 sockfd = start_reuseport_server(AF_INET6, SOCK_STREAM, NULL,
370 DUT_CTRL_PORT, 0, 1);
371 if (!sockfd) {
372 fprintf(stderr,
373 "Failed creating control socket on device %s\n", env.ifname);
374 return -errno;
375 }
376
377 ctrl_sockfd = accept(*sockfd, (struct sockaddr *)&ctrl_addr, &addrlen);
378 if (ctrl_sockfd < 0) {
379 fprintf(stderr,
380 "Failed accepting connections on device %s control socket\n",
381 env.ifname);
382 free_fds(sockfd, 1);
383 return -errno;
384 }
385
386 /* CTRL loop */
387 while (!exiting) {
388 unsigned char buf[BUFSIZE] = {};
389 struct tlv_hdr *tlv = (struct tlv_hdr *)buf;
390
391 err = recv_msg(ctrl_sockfd, buf, BUFSIZE, NULL, 0);
392 if (err)
393 continue;
394
395 switch (ntohs(tlv->type)) {
396 case CMD_START: {
397 if (state == CMD_START)
398 continue;
399
400 state = CMD_START;
401 /* Load the XDP program on the DUT */
402 err = dut_attach_xdp_prog(skel, flags);
403 if (err)
404 goto out;
405
406 err = dut_run_echo_thread(&dut_thread, &echo_sockfd);
407 if (err < 0)
408 goto out;
409
410 tlv->type = htons(CMD_ACK);
411 tlv->len = htons(sizeof(*tlv));
412 err = send(ctrl_sockfd, buf, sizeof(*tlv), 0);
413 if (err < 0)
414 goto end_thread;
415 break;
416 }
417 case CMD_STOP:
418 if (state != CMD_START)
419 break;
420
421 state = CMD_STOP;
422
423 exiting = true;
424 bpf_xdp_detach(env.ifindex, flags, NULL);
425
426 tlv->type = htons(CMD_ACK);
427 tlv->len = htons(sizeof(*tlv));
428 err = send(ctrl_sockfd, buf, sizeof(*tlv), 0);
429 goto end_thread;
430 case CMD_GET_XDP_CAP: {
431 LIBBPF_OPTS(bpf_xdp_query_opts, opts);
432 unsigned long long val;
433 size_t n;
434
435 err = bpf_xdp_query(env.ifindex, XDP_FLAGS_DRV_MODE,
436 &opts);
437 if (err) {
438 fprintf(stderr,
439 "Failed querying XDP cap for device %s\n",
440 env.ifname);
441 goto end_thread;
442 }
443
444 tlv->type = htons(CMD_ACK);
445 n = sizeof(*tlv) + sizeof(opts.feature_flags);
446 tlv->len = htons(n);
447
448 val = htobe64(opts.feature_flags);
449 memcpy(tlv->data, &val, sizeof(val));
450
451 err = send(ctrl_sockfd, buf, n, 0);
452 if (err < 0)
453 goto end_thread;
454 break;
455 }
456 case CMD_GET_STATS: {
457 unsigned int key = 0, val;
458 size_t n;
459
460 err = bpf_map__lookup_elem(skel->maps.dut_stats,
461 &key, sizeof(key),
462 &val, sizeof(val), 0);
463 if (err) {
464 fprintf(stderr,
465 "bpf_map_lookup_elem failed (%d)\n", err);
466 goto end_thread;
467 }
468
469 tlv->type = htons(CMD_ACK);
470 n = sizeof(*tlv) + sizeof(val);
471 tlv->len = htons(n);
472
473 val = htonl(val);
474 memcpy(tlv->data, &val, sizeof(val));
475
476 err = send(ctrl_sockfd, buf, n, 0);
477 if (err < 0)
478 goto end_thread;
479 break;
480 }
481 default:
482 break;
483 }
484 }
485
486 end_thread:
487 pthread_join(dut_thread, NULL);
488 out:
489 bpf_xdp_detach(env.ifindex, flags, NULL);
490 close(ctrl_sockfd);
491 free_fds(sockfd, 1);
492
493 return err;
494 }
495
tester_collect_detected_cap(struct xdp_features * skel,unsigned int dut_stats)496 static bool tester_collect_detected_cap(struct xdp_features *skel,
497 unsigned int dut_stats)
498 {
499 unsigned int err, key = 0, val;
500
501 if (!dut_stats)
502 return false;
503
504 err = bpf_map__lookup_elem(skel->maps.stats, &key, sizeof(key),
505 &val, sizeof(val), 0);
506 if (err) {
507 fprintf(stderr, "bpf_map_lookup_elem failed (%d)\n", err);
508 return false;
509 }
510
511 switch (env.feature.action) {
512 case XDP_PASS:
513 case XDP_TX:
514 case XDP_REDIRECT:
515 return val > 0;
516 case XDP_DROP:
517 case XDP_ABORTED:
518 return val == 0;
519 default:
520 break;
521 }
522
523 if (env.feature.drv_feature == NETDEV_XDP_ACT_NDO_XMIT)
524 return val > 0;
525
526 return false;
527 }
528
send_and_recv_msg(int sockfd,enum test_commands cmd,void * val,size_t val_size)529 static int send_and_recv_msg(int sockfd, enum test_commands cmd, void *val,
530 size_t val_size)
531 {
532 unsigned char buf[BUFSIZE] = {};
533 struct tlv_hdr *tlv = (struct tlv_hdr *)buf;
534 int err;
535
536 tlv->type = htons(cmd);
537 tlv->len = htons(sizeof(*tlv));
538
539 err = send(sockfd, buf, sizeof(*tlv), 0);
540 if (err < 0)
541 return err;
542
543 err = recv_msg(sockfd, buf, BUFSIZE, val, val_size);
544 if (err < 0)
545 return err;
546
547 return ntohs(tlv->type) == CMD_ACK ? 0 : -EINVAL;
548 }
549
send_echo_msg(void)550 static int send_echo_msg(void)
551 {
552 unsigned char buf[sizeof(struct tlv_hdr)];
553 struct tlv_hdr *tlv = (struct tlv_hdr *)buf;
554 int sockfd, n;
555
556 sockfd = socket(AF_INET6, SOCK_DGRAM, 0);
557 if (sockfd < 0) {
558 fprintf(stderr,
559 "Failed creating data UDP socket on device %s\n",
560 env.ifname);
561 return -errno;
562 }
563
564 tlv->type = htons(CMD_ECHO);
565 tlv->len = htons(sizeof(*tlv));
566
567 n = sendto(sockfd, buf, sizeof(*tlv), MSG_NOSIGNAL | MSG_CONFIRM,
568 (struct sockaddr *)&env.dut_addr, sizeof(env.dut_addr));
569 close(sockfd);
570
571 return n == ntohs(tlv->len) ? 0 : -EINVAL;
572 }
573
tester_run(struct xdp_features * skel)574 static int tester_run(struct xdp_features *skel)
575 {
576 int flags = XDP_FLAGS_UPDATE_IF_NOEXIST | XDP_FLAGS_DRV_MODE;
577 unsigned long long advertised_feature;
578 struct bpf_program *prog;
579 unsigned int stats;
580 int i, err, sockfd;
581 bool detected_cap;
582
583 sockfd = socket(AF_INET6, SOCK_STREAM, 0);
584 if (sockfd < 0) {
585 fprintf(stderr,
586 "Failed creating tester service control socket\n");
587 return -errno;
588 }
589
590 if (settimeo(sockfd, 1000) < 0)
591 return -EINVAL;
592
593 err = connect(sockfd, (struct sockaddr *)&env.dut_ctrl_addr,
594 sizeof(env.dut_ctrl_addr));
595 if (err) {
596 fprintf(stderr,
597 "Failed connecting to the Device Under Test control socket\n");
598 return -errno;
599 }
600
601 err = send_and_recv_msg(sockfd, CMD_GET_XDP_CAP, &advertised_feature,
602 sizeof(advertised_feature));
603 if (err < 0) {
604 close(sockfd);
605 return err;
606 }
607
608 advertised_feature = be64toh(advertised_feature);
609
610 if (env.feature.drv_feature == NETDEV_XDP_ACT_NDO_XMIT ||
611 env.feature.action == XDP_TX)
612 prog = skel->progs.xdp_tester_check_tx;
613 else
614 prog = skel->progs.xdp_tester_check_rx;
615
616 err = bpf_xdp_attach(env.ifindex, bpf_program__fd(prog), flags, NULL);
617 if (err) {
618 fprintf(stderr, "Failed attaching XDP program to device %s\n",
619 env.ifname);
620 goto out;
621 }
622
623 err = send_and_recv_msg(sockfd, CMD_START, NULL, 0);
624 if (err)
625 goto out;
626
627 for (i = 0; i < 10 && !exiting; i++) {
628 err = send_echo_msg();
629 if (err < 0)
630 goto out;
631
632 sleep(1);
633 }
634
635 err = send_and_recv_msg(sockfd, CMD_GET_STATS, &stats, sizeof(stats));
636 if (err)
637 goto out;
638
639 /* stop the test */
640 err = send_and_recv_msg(sockfd, CMD_STOP, NULL, 0);
641 /* send a new echo message to wake echo thread of the dut */
642 send_echo_msg();
643
644 detected_cap = tester_collect_detected_cap(skel, ntohl(stats));
645
646 fprintf(stdout, "Feature %s: [%s][%s]\n", get_xdp_feature_str(),
647 detected_cap ? GREEN("DETECTED") : RED("NOT DETECTED"),
648 env.feature.drv_feature & advertised_feature ? GREEN("ADVERTISED")
649 : RED("NOT ADVERTISED"));
650 out:
651 bpf_xdp_detach(env.ifindex, flags, NULL);
652 close(sockfd);
653 return err < 0 ? err : 0;
654 }
655
main(int argc,char ** argv)656 int main(int argc, char **argv)
657 {
658 struct xdp_features *skel;
659 int err;
660
661 libbpf_set_strict_mode(LIBBPF_STRICT_ALL);
662 libbpf_set_print(libbpf_print_fn);
663
664 signal(SIGINT, sig_handler);
665 signal(SIGTERM, sig_handler);
666
667 set_env_default();
668
669 /* Parse command line arguments */
670 err = argp_parse(&argp, argc, argv, 0, NULL, NULL);
671 if (err)
672 return err;
673
674 if (env.ifindex < 0) {
675 fprintf(stderr, "Invalid device name %s\n", env.ifname);
676 return -ENODEV;
677 }
678
679 /* Load and verify BPF application */
680 skel = xdp_features__open();
681 if (!skel) {
682 fprintf(stderr, "Failed to open and load BPF skeleton\n");
683 return -EINVAL;
684 }
685
686 skel->rodata->tester_addr =
687 ((struct sockaddr_in6 *)&env.tester_addr)->sin6_addr;
688 skel->rodata->dut_addr =
689 ((struct sockaddr_in6 *)&env.dut_addr)->sin6_addr;
690
691 /* Load & verify BPF programs */
692 err = xdp_features__load(skel);
693 if (err) {
694 fprintf(stderr, "Failed to load and verify BPF skeleton\n");
695 goto cleanup;
696 }
697
698 err = xdp_features__attach(skel);
699 if (err) {
700 fprintf(stderr, "Failed to attach BPF skeleton\n");
701 goto cleanup;
702 }
703
704 if (env.is_tester) {
705 /* Tester */
706 fprintf(stdout, "Starting tester service on device %s\n",
707 env.ifname);
708 err = tester_run(skel);
709 } else {
710 /* DUT */
711 fprintf(stdout, "Starting test on device %s\n", env.ifname);
712 err = dut_run(skel);
713 }
714
715 cleanup:
716 xdp_features__destroy(skel);
717
718 return err < 0 ? -err : 0;
719 }
720