1 // SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) 2 /* Copyright (c) 2019 Netronome Systems, Inc. */ 3 4 #include <ctype.h> 5 #include <errno.h> 6 #include <string.h> 7 #include <unistd.h> 8 #include <net/if.h> 9 #include <sys/utsname.h> 10 #include <sys/vfs.h> 11 12 #include <linux/filter.h> 13 #include <linux/limits.h> 14 15 #include <bpf/bpf.h> 16 #include <bpf/libbpf.h> 17 #include <zlib.h> 18 19 #include "main.h" 20 21 #ifndef PROC_SUPER_MAGIC 22 # define PROC_SUPER_MAGIC 0x9fa0 23 #endif 24 25 enum probe_component { 26 COMPONENT_UNSPEC, 27 COMPONENT_KERNEL, 28 COMPONENT_DEVICE, 29 }; 30 31 #define BPF_HELPER_MAKE_ENTRY(name) [BPF_FUNC_ ## name] = "bpf_" # name 32 static const char * const helper_name[] = { 33 __BPF_FUNC_MAPPER(BPF_HELPER_MAKE_ENTRY) 34 }; 35 36 #undef BPF_HELPER_MAKE_ENTRY 37 38 /* Miscellaneous utility functions */ 39 40 static bool check_procfs(void) 41 { 42 struct statfs st_fs; 43 44 if (statfs("/proc", &st_fs) < 0) 45 return false; 46 if ((unsigned long)st_fs.f_type != PROC_SUPER_MAGIC) 47 return false; 48 49 return true; 50 } 51 52 static void uppercase(char *str, size_t len) 53 { 54 size_t i; 55 56 for (i = 0; i < len && str[i] != '\0'; i++) 57 str[i] = toupper(str[i]); 58 } 59 60 /* Printing utility functions */ 61 62 static void 63 print_bool_feature(const char *feat_name, const char *plain_name, 64 const char *define_name, bool res, const char *define_prefix) 65 { 66 if (json_output) 67 jsonw_bool_field(json_wtr, feat_name, res); 68 else if (define_prefix) 69 printf("#define %s%sHAVE_%s\n", define_prefix, 70 res ? "" : "NO_", define_name); 71 else 72 printf("%s is %savailable\n", plain_name, res ? "" : "NOT "); 73 } 74 75 static void print_kernel_option(const char *name, const char *value) 76 { 77 char *endptr; 78 int res; 79 80 /* No support for C-style ouptut */ 81 82 if (json_output) { 83 if (!value) { 84 jsonw_null_field(json_wtr, name); 85 return; 86 } 87 errno = 0; 88 res = strtol(value, &endptr, 0); 89 if (!errno && *endptr == '\n') 90 jsonw_int_field(json_wtr, name, res); 91 else 92 jsonw_string_field(json_wtr, name, value); 93 } else { 94 if (value) 95 printf("%s is set to %s\n", name, value); 96 else 97 printf("%s is not set\n", name); 98 } 99 } 100 101 static void 102 print_start_section(const char *json_title, const char *plain_title, 103 const char *define_comment, const char *define_prefix) 104 { 105 if (json_output) { 106 jsonw_name(json_wtr, json_title); 107 jsonw_start_object(json_wtr); 108 } else if (define_prefix) { 109 printf("%s\n", define_comment); 110 } else { 111 printf("%s\n", plain_title); 112 } 113 } 114 115 static void 116 print_end_then_start_section(const char *json_title, const char *plain_title, 117 const char *define_comment, 118 const char *define_prefix) 119 { 120 if (json_output) 121 jsonw_end_object(json_wtr); 122 else 123 printf("\n"); 124 125 print_start_section(json_title, plain_title, define_comment, 126 define_prefix); 127 } 128 129 /* Probing functions */ 130 131 static int read_procfs(const char *path) 132 { 133 char *endptr, *line = NULL; 134 size_t len = 0; 135 FILE *fd; 136 int res; 137 138 fd = fopen(path, "r"); 139 if (!fd) 140 return -1; 141 142 res = getline(&line, &len, fd); 143 fclose(fd); 144 if (res < 0) 145 return -1; 146 147 errno = 0; 148 res = strtol(line, &endptr, 10); 149 if (errno || *line == '\0' || *endptr != '\n') 150 res = -1; 151 free(line); 152 153 return res; 154 } 155 156 static void probe_unprivileged_disabled(void) 157 { 158 int res; 159 160 /* No support for C-style ouptut */ 161 162 res = read_procfs("/proc/sys/kernel/unprivileged_bpf_disabled"); 163 if (json_output) { 164 jsonw_int_field(json_wtr, "unprivileged_bpf_disabled", res); 165 } else { 166 switch (res) { 167 case 0: 168 printf("bpf() syscall for unprivileged users is enabled\n"); 169 break; 170 case 1: 171 printf("bpf() syscall restricted to privileged users\n"); 172 break; 173 case -1: 174 printf("Unable to retrieve required privileges for bpf() syscall\n"); 175 break; 176 default: 177 printf("bpf() syscall restriction has unknown value %d\n", res); 178 } 179 } 180 } 181 182 static void probe_jit_enable(void) 183 { 184 int res; 185 186 /* No support for C-style ouptut */ 187 188 res = read_procfs("/proc/sys/net/core/bpf_jit_enable"); 189 if (json_output) { 190 jsonw_int_field(json_wtr, "bpf_jit_enable", res); 191 } else { 192 switch (res) { 193 case 0: 194 printf("JIT compiler is disabled\n"); 195 break; 196 case 1: 197 printf("JIT compiler is enabled\n"); 198 break; 199 case 2: 200 printf("JIT compiler is enabled with debugging traces in kernel logs\n"); 201 break; 202 case -1: 203 printf("Unable to retrieve JIT-compiler status\n"); 204 break; 205 default: 206 printf("JIT-compiler status has unknown value %d\n", 207 res); 208 } 209 } 210 } 211 212 static void probe_jit_harden(void) 213 { 214 int res; 215 216 /* No support for C-style ouptut */ 217 218 res = read_procfs("/proc/sys/net/core/bpf_jit_harden"); 219 if (json_output) { 220 jsonw_int_field(json_wtr, "bpf_jit_harden", res); 221 } else { 222 switch (res) { 223 case 0: 224 printf("JIT compiler hardening is disabled\n"); 225 break; 226 case 1: 227 printf("JIT compiler hardening is enabled for unprivileged users\n"); 228 break; 229 case 2: 230 printf("JIT compiler hardening is enabled for all users\n"); 231 break; 232 case -1: 233 printf("Unable to retrieve JIT hardening status\n"); 234 break; 235 default: 236 printf("JIT hardening status has unknown value %d\n", 237 res); 238 } 239 } 240 } 241 242 static void probe_jit_kallsyms(void) 243 { 244 int res; 245 246 /* No support for C-style ouptut */ 247 248 res = read_procfs("/proc/sys/net/core/bpf_jit_kallsyms"); 249 if (json_output) { 250 jsonw_int_field(json_wtr, "bpf_jit_kallsyms", res); 251 } else { 252 switch (res) { 253 case 0: 254 printf("JIT compiler kallsyms exports are disabled\n"); 255 break; 256 case 1: 257 printf("JIT compiler kallsyms exports are enabled for root\n"); 258 break; 259 case -1: 260 printf("Unable to retrieve JIT kallsyms export status\n"); 261 break; 262 default: 263 printf("JIT kallsyms exports status has unknown value %d\n", res); 264 } 265 } 266 } 267 268 static void probe_jit_limit(void) 269 { 270 int res; 271 272 /* No support for C-style ouptut */ 273 274 res = read_procfs("/proc/sys/net/core/bpf_jit_limit"); 275 if (json_output) { 276 jsonw_int_field(json_wtr, "bpf_jit_limit", res); 277 } else { 278 switch (res) { 279 case -1: 280 printf("Unable to retrieve global memory limit for JIT compiler for unprivileged users\n"); 281 break; 282 default: 283 printf("Global memory limit for JIT compiler for unprivileged users is %d bytes\n", res); 284 } 285 } 286 } 287 288 static bool read_next_kernel_config_option(gzFile file, char *buf, size_t n, 289 char **value) 290 { 291 char *sep; 292 293 while (gzgets(file, buf, n)) { 294 if (strncmp(buf, "CONFIG_", 7)) 295 continue; 296 297 sep = strchr(buf, '='); 298 if (!sep) 299 continue; 300 301 /* Trim ending '\n' */ 302 buf[strlen(buf) - 1] = '\0'; 303 304 /* Split on '=' and ensure that a value is present. */ 305 *sep = '\0'; 306 if (!sep[1]) 307 continue; 308 309 *value = sep + 1; 310 return true; 311 } 312 313 return false; 314 } 315 316 static void probe_kernel_image_config(void) 317 { 318 static const char * const options[] = { 319 /* Enable BPF */ 320 "CONFIG_BPF", 321 /* Enable bpf() syscall */ 322 "CONFIG_BPF_SYSCALL", 323 /* Does selected architecture support eBPF JIT compiler */ 324 "CONFIG_HAVE_EBPF_JIT", 325 /* Compile eBPF JIT compiler */ 326 "CONFIG_BPF_JIT", 327 /* Avoid compiling eBPF interpreter (use JIT only) */ 328 "CONFIG_BPF_JIT_ALWAYS_ON", 329 330 /* cgroups */ 331 "CONFIG_CGROUPS", 332 /* BPF programs attached to cgroups */ 333 "CONFIG_CGROUP_BPF", 334 /* bpf_get_cgroup_classid() helper */ 335 "CONFIG_CGROUP_NET_CLASSID", 336 /* bpf_skb_{,ancestor_}cgroup_id() helpers */ 337 "CONFIG_SOCK_CGROUP_DATA", 338 339 /* Tracing: attach BPF to kprobes, tracepoints, etc. */ 340 "CONFIG_BPF_EVENTS", 341 /* Kprobes */ 342 "CONFIG_KPROBE_EVENTS", 343 /* Uprobes */ 344 "CONFIG_UPROBE_EVENTS", 345 /* Tracepoints */ 346 "CONFIG_TRACING", 347 /* Syscall tracepoints */ 348 "CONFIG_FTRACE_SYSCALLS", 349 /* bpf_override_return() helper support for selected arch */ 350 "CONFIG_FUNCTION_ERROR_INJECTION", 351 /* bpf_override_return() helper */ 352 "CONFIG_BPF_KPROBE_OVERRIDE", 353 354 /* Network */ 355 "CONFIG_NET", 356 /* AF_XDP sockets */ 357 "CONFIG_XDP_SOCKETS", 358 /* BPF_PROG_TYPE_LWT_* and related helpers */ 359 "CONFIG_LWTUNNEL_BPF", 360 /* BPF_PROG_TYPE_SCHED_ACT, TC (traffic control) actions */ 361 "CONFIG_NET_ACT_BPF", 362 /* BPF_PROG_TYPE_SCHED_CLS, TC filters */ 363 "CONFIG_NET_CLS_BPF", 364 /* TC clsact qdisc */ 365 "CONFIG_NET_CLS_ACT", 366 /* Ingress filtering with TC */ 367 "CONFIG_NET_SCH_INGRESS", 368 /* bpf_skb_get_xfrm_state() helper */ 369 "CONFIG_XFRM", 370 /* bpf_get_route_realm() helper */ 371 "CONFIG_IP_ROUTE_CLASSID", 372 /* BPF_PROG_TYPE_LWT_SEG6_LOCAL and related helpers */ 373 "CONFIG_IPV6_SEG6_BPF", 374 /* BPF_PROG_TYPE_LIRC_MODE2 and related helpers */ 375 "CONFIG_BPF_LIRC_MODE2", 376 /* BPF stream parser and BPF socket maps */ 377 "CONFIG_BPF_STREAM_PARSER", 378 /* xt_bpf module for passing BPF programs to netfilter */ 379 "CONFIG_NETFILTER_XT_MATCH_BPF", 380 /* bpfilter back-end for iptables */ 381 "CONFIG_BPFILTER", 382 /* bpftilter module with "user mode helper" */ 383 "CONFIG_BPFILTER_UMH", 384 385 /* test_bpf module for BPF tests */ 386 "CONFIG_TEST_BPF", 387 }; 388 char *values[ARRAY_SIZE(options)] = { }; 389 struct utsname utsn; 390 char path[PATH_MAX]; 391 gzFile file = NULL; 392 char buf[4096]; 393 char *value; 394 size_t i; 395 396 if (!uname(&utsn)) { 397 snprintf(path, sizeof(path), "/boot/config-%s", utsn.release); 398 399 /* gzopen also accepts uncompressed files. */ 400 file = gzopen(path, "r"); 401 } 402 403 if (!file) { 404 /* Some distributions build with CONFIG_IKCONFIG=y and put the 405 * config file at /proc/config.gz. 406 */ 407 file = gzopen("/proc/config.gz", "r"); 408 } 409 if (!file) { 410 p_info("skipping kernel config, can't open file: %s", 411 strerror(errno)); 412 goto end_parse; 413 } 414 /* Sanity checks */ 415 if (!gzgets(file, buf, sizeof(buf)) || 416 !gzgets(file, buf, sizeof(buf))) { 417 p_info("skipping kernel config, can't read from file: %s", 418 strerror(errno)); 419 goto end_parse; 420 } 421 if (strcmp(buf, "# Automatically generated file; DO NOT EDIT.\n")) { 422 p_info("skipping kernel config, can't find correct file"); 423 goto end_parse; 424 } 425 426 while (read_next_kernel_config_option(file, buf, sizeof(buf), &value)) { 427 for (i = 0; i < ARRAY_SIZE(options); i++) { 428 if (values[i] || strcmp(buf, options[i])) 429 continue; 430 431 values[i] = strdup(value); 432 } 433 } 434 435 end_parse: 436 if (file) 437 gzclose(file); 438 439 for (i = 0; i < ARRAY_SIZE(options); i++) { 440 print_kernel_option(options[i], values[i]); 441 free(values[i]); 442 } 443 } 444 445 static bool probe_bpf_syscall(const char *define_prefix) 446 { 447 bool res; 448 449 bpf_load_program(BPF_PROG_TYPE_UNSPEC, NULL, 0, NULL, 0, NULL, 0); 450 res = (errno != ENOSYS); 451 452 print_bool_feature("have_bpf_syscall", 453 "bpf() syscall", 454 "BPF_SYSCALL", 455 res, define_prefix); 456 457 return res; 458 } 459 460 static void 461 probe_prog_type(enum bpf_prog_type prog_type, bool *supported_types, 462 const char *define_prefix, __u32 ifindex) 463 { 464 char feat_name[128], plain_desc[128], define_name[128]; 465 const char *plain_comment = "eBPF program_type "; 466 size_t maxlen; 467 bool res; 468 469 if (ifindex) 470 /* Only test offload-able program types */ 471 switch (prog_type) { 472 case BPF_PROG_TYPE_SCHED_CLS: 473 case BPF_PROG_TYPE_XDP: 474 break; 475 default: 476 return; 477 } 478 479 res = bpf_probe_prog_type(prog_type, ifindex); 480 481 supported_types[prog_type] |= res; 482 483 maxlen = sizeof(plain_desc) - strlen(plain_comment) - 1; 484 if (strlen(prog_type_name[prog_type]) > maxlen) { 485 p_info("program type name too long"); 486 return; 487 } 488 489 sprintf(feat_name, "have_%s_prog_type", prog_type_name[prog_type]); 490 sprintf(define_name, "%s_prog_type", prog_type_name[prog_type]); 491 uppercase(define_name, sizeof(define_name)); 492 sprintf(plain_desc, "%s%s", plain_comment, prog_type_name[prog_type]); 493 print_bool_feature(feat_name, plain_desc, define_name, res, 494 define_prefix); 495 } 496 497 static void 498 probe_map_type(enum bpf_map_type map_type, const char *define_prefix, 499 __u32 ifindex) 500 { 501 char feat_name[128], plain_desc[128], define_name[128]; 502 const char *plain_comment = "eBPF map_type "; 503 size_t maxlen; 504 bool res; 505 506 res = bpf_probe_map_type(map_type, ifindex); 507 508 maxlen = sizeof(plain_desc) - strlen(plain_comment) - 1; 509 if (strlen(map_type_name[map_type]) > maxlen) { 510 p_info("map type name too long"); 511 return; 512 } 513 514 sprintf(feat_name, "have_%s_map_type", map_type_name[map_type]); 515 sprintf(define_name, "%s_map_type", map_type_name[map_type]); 516 uppercase(define_name, sizeof(define_name)); 517 sprintf(plain_desc, "%s%s", plain_comment, map_type_name[map_type]); 518 print_bool_feature(feat_name, plain_desc, define_name, res, 519 define_prefix); 520 } 521 522 static void 523 probe_helpers_for_progtype(enum bpf_prog_type prog_type, bool supported_type, 524 const char *define_prefix, __u32 ifindex) 525 { 526 const char *ptype_name = prog_type_name[prog_type]; 527 char feat_name[128]; 528 unsigned int id; 529 bool res; 530 531 if (ifindex) 532 /* Only test helpers for offload-able program types */ 533 switch (prog_type) { 534 case BPF_PROG_TYPE_SCHED_CLS: 535 case BPF_PROG_TYPE_XDP: 536 break; 537 default: 538 return; 539 } 540 541 if (json_output) { 542 sprintf(feat_name, "%s_available_helpers", ptype_name); 543 jsonw_name(json_wtr, feat_name); 544 jsonw_start_array(json_wtr); 545 } else if (!define_prefix) { 546 printf("eBPF helpers supported for program type %s:", 547 ptype_name); 548 } 549 550 for (id = 1; id < ARRAY_SIZE(helper_name); id++) { 551 if (!supported_type) 552 res = false; 553 else 554 res = bpf_probe_helper(id, prog_type, ifindex); 555 556 if (json_output) { 557 if (res) 558 jsonw_string(json_wtr, helper_name[id]); 559 } else if (define_prefix) { 560 printf("#define %sBPF__PROG_TYPE_%s__HELPER_%s %s\n", 561 define_prefix, ptype_name, helper_name[id], 562 res ? "1" : "0"); 563 } else { 564 if (res) 565 printf("\n\t- %s", helper_name[id]); 566 } 567 } 568 569 if (json_output) 570 jsonw_end_array(json_wtr); 571 else if (!define_prefix) 572 printf("\n"); 573 } 574 575 static void 576 probe_large_insn_limit(const char *define_prefix, __u32 ifindex) 577 { 578 bool res; 579 580 res = bpf_probe_large_insn_limit(ifindex); 581 print_bool_feature("have_large_insn_limit", 582 "Large program size limit", 583 "LARGE_INSN_LIMIT", 584 res, define_prefix); 585 } 586 587 static int do_probe(int argc, char **argv) 588 { 589 enum probe_component target = COMPONENT_UNSPEC; 590 const char *define_prefix = NULL; 591 bool supported_types[128] = {}; 592 __u32 ifindex = 0; 593 unsigned int i; 594 char *ifname; 595 596 /* Detection assumes user has sufficient privileges (CAP_SYS_ADMIN). 597 * Let's approximate, and restrict usage to root user only. 598 */ 599 if (geteuid()) { 600 p_err("please run this command as root user"); 601 return -1; 602 } 603 604 set_max_rlimit(); 605 606 while (argc) { 607 if (is_prefix(*argv, "kernel")) { 608 if (target != COMPONENT_UNSPEC) { 609 p_err("component to probe already specified"); 610 return -1; 611 } 612 target = COMPONENT_KERNEL; 613 NEXT_ARG(); 614 } else if (is_prefix(*argv, "dev")) { 615 NEXT_ARG(); 616 617 if (target != COMPONENT_UNSPEC || ifindex) { 618 p_err("component to probe already specified"); 619 return -1; 620 } 621 if (!REQ_ARGS(1)) 622 return -1; 623 624 target = COMPONENT_DEVICE; 625 ifname = GET_ARG(); 626 ifindex = if_nametoindex(ifname); 627 if (!ifindex) { 628 p_err("unrecognized netdevice '%s': %s", ifname, 629 strerror(errno)); 630 return -1; 631 } 632 } else if (is_prefix(*argv, "macros") && !define_prefix) { 633 define_prefix = ""; 634 NEXT_ARG(); 635 } else if (is_prefix(*argv, "prefix")) { 636 if (!define_prefix) { 637 p_err("'prefix' argument can only be use after 'macros'"); 638 return -1; 639 } 640 if (strcmp(define_prefix, "")) { 641 p_err("'prefix' already defined"); 642 return -1; 643 } 644 NEXT_ARG(); 645 646 if (!REQ_ARGS(1)) 647 return -1; 648 define_prefix = GET_ARG(); 649 } else { 650 p_err("expected no more arguments, 'kernel', 'dev', 'macros' or 'prefix', got: '%s'?", 651 *argv); 652 return -1; 653 } 654 } 655 656 if (json_output) { 657 define_prefix = NULL; 658 jsonw_start_object(json_wtr); 659 } 660 661 switch (target) { 662 case COMPONENT_KERNEL: 663 case COMPONENT_UNSPEC: 664 if (define_prefix) 665 break; 666 667 print_start_section("system_config", 668 "Scanning system configuration...", 669 NULL, /* define_comment never used here */ 670 NULL); /* define_prefix always NULL here */ 671 if (check_procfs()) { 672 probe_unprivileged_disabled(); 673 probe_jit_enable(); 674 probe_jit_harden(); 675 probe_jit_kallsyms(); 676 probe_jit_limit(); 677 } else { 678 p_info("/* procfs not mounted, skipping related probes */"); 679 } 680 probe_kernel_image_config(); 681 if (json_output) 682 jsonw_end_object(json_wtr); 683 else 684 printf("\n"); 685 break; 686 default: 687 break; 688 } 689 690 print_start_section("syscall_config", 691 "Scanning system call availability...", 692 "/*** System call availability ***/", 693 define_prefix); 694 695 if (!probe_bpf_syscall(define_prefix)) 696 /* bpf() syscall unavailable, don't probe other BPF features */ 697 goto exit_close_json; 698 699 print_end_then_start_section("program_types", 700 "Scanning eBPF program types...", 701 "/*** eBPF program types ***/", 702 define_prefix); 703 704 for (i = BPF_PROG_TYPE_UNSPEC + 1; i < ARRAY_SIZE(prog_type_name); i++) 705 probe_prog_type(i, supported_types, define_prefix, ifindex); 706 707 print_end_then_start_section("map_types", 708 "Scanning eBPF map types...", 709 "/*** eBPF map types ***/", 710 define_prefix); 711 712 for (i = BPF_MAP_TYPE_UNSPEC + 1; i < map_type_name_size; i++) 713 probe_map_type(i, define_prefix, ifindex); 714 715 print_end_then_start_section("helpers", 716 "Scanning eBPF helper functions...", 717 "/*** eBPF helper functions ***/", 718 define_prefix); 719 720 if (define_prefix) 721 printf("/*\n" 722 " * Use %sHAVE_PROG_TYPE_HELPER(prog_type_name, helper_name)\n" 723 " * to determine if <helper_name> is available for <prog_type_name>,\n" 724 " * e.g.\n" 725 " * #if %sHAVE_PROG_TYPE_HELPER(xdp, bpf_redirect)\n" 726 " * // do stuff with this helper\n" 727 " * #elif\n" 728 " * // use a workaround\n" 729 " * #endif\n" 730 " */\n" 731 "#define %sHAVE_PROG_TYPE_HELPER(prog_type, helper) \\\n" 732 " %sBPF__PROG_TYPE_ ## prog_type ## __HELPER_ ## helper\n", 733 define_prefix, define_prefix, define_prefix, 734 define_prefix); 735 for (i = BPF_PROG_TYPE_UNSPEC + 1; i < ARRAY_SIZE(prog_type_name); i++) 736 probe_helpers_for_progtype(i, supported_types[i], 737 define_prefix, ifindex); 738 739 print_end_then_start_section("misc", 740 "Scanning miscellaneous eBPF features...", 741 "/*** eBPF misc features ***/", 742 define_prefix); 743 probe_large_insn_limit(define_prefix, ifindex); 744 745 exit_close_json: 746 if (json_output) { 747 /* End current "section" of probes */ 748 jsonw_end_object(json_wtr); 749 /* End root object */ 750 jsonw_end_object(json_wtr); 751 } 752 753 return 0; 754 } 755 756 static int do_help(int argc, char **argv) 757 { 758 if (json_output) { 759 jsonw_null(json_wtr); 760 return 0; 761 } 762 763 fprintf(stderr, 764 "Usage: %s %s probe [COMPONENT] [macros [prefix PREFIX]]\n" 765 " %s %s help\n" 766 "\n" 767 " COMPONENT := { kernel | dev NAME }\n" 768 "", 769 bin_name, argv[-2], bin_name, argv[-2]); 770 771 return 0; 772 } 773 774 static const struct cmd cmds[] = { 775 { "probe", do_probe }, 776 { "help", do_help }, 777 { 0 } 778 }; 779 780 int do_feature(int argc, char **argv) 781 { 782 return cmd_select(cmds, argc, argv, do_help); 783 } 784