]> The Tcpdump Group git mirrors - libpcap/commitdiff
Start testing filter programs on savefiles.
authorDenis Ovsienko <[email protected]>
Wed, 29 Jan 2025 11:24:08 +0000 (11:24 +0000)
committerDenis Ovsienko <[email protected]>
Thu, 30 Jan 2025 19:45:58 +0000 (19:45 +0000)
In filtertest implement a new "-r <savefile>" mode, which instead of
printing the filter program opens the specified savefile, applies the
program to each packet and prints the results.

In TESTrun implement a new "apply" type of test, which uses the new
filtertest mode and compares the printed and the expected results.
Declare "apply" test blocks for PPPoE, DECnet and MTP3 and add the
associated savefiles.  Factor validate_stdout_test() out.  Add
subroutines to label and to run "apply" tests.  Add a single-threaded
loop to generate ready-to-run tests.

The isup_load_generator.pcap file is from the Wireshark packet capture
collection; decnet.pcap, loopback.pcap, pppoe.pcap and pppoes.pcap are
from tcpdump tests.  All files have been converted to .pcap and made no
longer than 10 packets each.

testprogs/TESTrun
testprogs/filtertest.c
tests/filter/decnet.pcap [new file with mode: 0644]
tests/filter/isup_load_generator.pcap [new file with mode: 0644]
tests/filter/loopback.pcap [new file with mode: 0644]
tests/filter/pppoe.pcap [new file with mode: 0644]
tests/filter/pppoes.pcap [new file with mode: 0644]

index 6b1f3d50828bb90888d981ab645efc6dcef63c6e..cdcdbb23d1d90179c1c059aae4ecb105cfbe0cc1 100755 (executable)
@@ -56,6 +56,8 @@ BEGIN {
        require $FindBin::RealBin . '/TEST' . ($Config{useithreads} ? 'mt' : 'st') . '.pm';
 }
 
+use constant SAVEFILE_DIR => $FindBin::RealBin . '/../tests/filter/';
+
 # When using threads, STDOUT becomes line-buffered on TTYs, which is not good
 # for interactive progress monitoring.
 STDOUT->autoflush (1) if $Config{useithreads} && -t STDOUT;
@@ -5557,6 +5559,235 @@ my %accept_blocks = (
        }, # dst_portrange_degenerate
 );
 
+# In apply_blocks the top-level keys are test block names.  Each test block
+# always generates two tests: optimized and unoptimized.  (Small tests often
+# produce short bytecode that is already optimal, in which case testing the
+# "optimized" version again is a duplicate work.  However, it is not clear yet
+# what would be the right way to avoid the duplicate work without creating
+# gaps in the test coverage.)  The top-level values are in turn hashes, where
+# the keys have the following meaning:
+#
+# * savefile (mandatory, string): the file in tests/filter/ to use with
+#   "filtertest -r", this should not have too many packets
+# * expr (mandatory, string): the filter expression
+# * results (mandatory, array): the list of program filter results to expect
+my %apply_blocks = (
+       pppoed_nullary_on_ctp => {
+               savefile => 'loopback.pcap',
+               expr => 'pppoed',
+               results => [0, 0, 0, 0, 0, 0],
+       },
+       pppoed_nullary_on_pppoed => {
+               savefile => 'pppoe.pcap',
+               expr => 'pppoed',
+               results => [1508],
+       },
+       pppoed_nullary_on_pppoes => {
+               savefile => 'pppoes.pcap',
+               expr => 'pppoed',
+               results => [0, 0],
+       },
+       pppoes_nullary_on_ctp => {
+               savefile => 'loopback.pcap',
+               expr => 'pppoes',
+               results => [0, 0, 0, 0, 0, 0],
+       },
+       pppoes_nullary_on_pppoed => {
+               savefile => 'pppoe.pcap',
+               expr => 'pppoes',
+               results => [0],
+       },
+       pppoes_nullary_on_pppoes => {
+               savefile => 'pppoes.pcap',
+               expr => 'pppoes',
+               results => [2000, 2000],
+       },
+       pppoes_unary_on_ctp => {
+               savefile => 'loopback.pcap',
+               expr => 'pppoes 0x3b',
+               results => [0, 0, 0, 0, 0, 0],
+       },
+       pppoes_unary_on_pppoed => {
+               savefile => 'pppoe.pcap',
+               expr => 'pppoes 0x3b',
+               results => [0],
+       },
+       pppoes_unary_on_pppoes => {
+               savefile => 'pppoes.pcap',
+               expr => 'pppoes 0x3b',
+               results => [0, 2000],
+       },
+       decnet_on_pppoed => {
+               savefile => 'pppoe.pcap',
+               expr => 'decnet',
+               results => [0],
+       },
+       decnet_on_decnet => {
+               savefile => 'decnet.pcap',
+               expr => 'decnet',
+               # This tests EtherType, so every packet matches.
+               results => [65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535],
+       },
+       decnet_src_1_1_on_pppoed => {
+               savefile => 'pppoe.pcap',
+               expr => 'decnet src 1.1',
+               results => [0],
+       },
+       decnet_dst_1_1_on_pppoed => {
+               savefile => 'pppoe.pcap',
+               expr => 'decnet dst 1.1',
+               results => [0],
+       },
+
+       # This tests a DECnet address, which in the current implementation works
+       # for data packets only.  The first packet is an Ethernet Endnode Hello
+       # message, so it does not match even though the packet is from node 1.1.
+       decnet_src_1_1_on_decnet => {
+               savefile => 'decnet.pcap',
+               expr => 'decnet src 1.1',
+               results => [0, 65535, 65535, 65535, 65535, 65535, 65535, 65535],
+       },
+       decnet_dst_1_1_on_decnet => {
+               savefile => 'decnet.pcap',
+               expr => 'decnet dst 1.1',
+               results => [0, 65535, 65535, 65535, 65535, 65535, 65535, 65535],
+       },
+       decnet_src_not_1_1_on_decnet => {
+               savefile => 'decnet.pcap',
+               expr => 'decnet src not 1.1',
+               results => [65535, 0, 0, 0, 0, 0, 0, 0],
+       },
+       decnet_dst_not_1_1_on_decnet => {
+               savefile => 'decnet.pcap',
+               expr => 'decnet dst not 1.1',
+               results => [65535, 0, 0, 0, 0, 0, 0, 0],
+       },
+
+       # The first result is correct from a formal point of view, but the actual
+       # reason is the same as above.
+       decnet_src_63_1023_on_decnet => {
+               savefile => 'decnet.pcap',
+               expr => 'decnet src 63.1023',
+               results => [0, 0, 0, 0, 0, 0, 0, 0],
+       },
+       decnet_dst_63_1023_on_decnet => {
+               savefile => 'decnet.pcap',
+               expr => 'decnet dst 63.1023',
+               results => [0, 0, 0, 0, 0, 0, 0, 0],
+       },
+       decnet_src_not_63_1023_on_decnet => {
+               savefile => 'decnet.pcap',
+               expr => 'decnet src not 63.1023',
+               results => [65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535],
+       },
+       decnet_dst_not_63_1023_on_decnet => {
+               savefile => 'decnet.pcap',
+               expr => 'decnet dst not 63.1023',
+               results => [65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535],
+       },
+
+       # The meaning of this expression is NOT the intuitive "any DECnet packets
+       # that do not have the source address set to 1.1", but this is not
+       # specific to DECnet.  Let's test it anyway.
+       decnet_src_not_1_1_on_pppoed => {
+               savefile => 'pppoe.pcap',
+               expr => 'decnet src not 1.1',
+               results => [1508],
+       },
+       decnet_dst_not_1_1_on_pppoed => {
+               savefile => 'pppoe.pcap',
+               expr => 'decnet dst not 1.1',
+               results => [1508],
+       },
+       decnet_src_not_63_1023_on_pppoed => {
+               savefile => 'pppoe.pcap',
+               expr => 'decnet src not 63.1023',
+               results => [1508],
+       },
+       decnet_dst_not_63_1023_on_pppoed => {
+               savefile => 'pppoe.pcap',
+               expr => 'decnet dst not 63.1023',
+               results => [1508],
+       },
+
+       decnet_src_63_1023_on_pppoed => {
+               savefile => 'pppoe.pcap',
+               expr => 'decnet src 63.1023',
+               results => [0],
+       },
+       decnet_dst_63_1023_on_pppoed => {
+               savefile => 'pppoe.pcap',
+               expr => 'decnet src 63.1023',
+               results => [0],
+       },
+
+       dpc_eq_1 => {
+               savefile => 'isup_load_generator.pcap',
+               expr => 'dpc == 1',
+               results => [0, 279, 0, 279, 279, 0, 279, 0, 279, 0],
+       },
+       dpc_lt_2 => {
+               savefile => 'isup_load_generator.pcap',
+               expr => 'dpc < 2',
+               results => [0, 279, 0, 279, 279, 0, 279, 0, 279, 0],
+       },
+       dpc_eq_2 => {
+               savefile => 'isup_load_generator.pcap',
+               expr => 'dpc == 2',
+               results => [279, 0, 279, 0, 0, 279, 0, 279, 0, 279],
+       },
+       dpc_gt_2 => {
+               savefile => 'isup_load_generator.pcap',
+               expr => 'dpc > 2',
+               results => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+       },
+       dpc_le_2 => {
+               savefile => 'isup_load_generator.pcap',
+               expr => 'dpc <= 2',
+               results => [279, 279, 279, 279, 279, 279, 279, 279, 279, 279],
+       },
+       dpc_ne_0 => {
+               savefile => 'isup_load_generator.pcap',
+               expr => 'dpc != 0',
+               results => [279, 279, 279, 279, 279, 279, 279, 279, 279, 279],
+       },
+       opc_eq_2 => {
+               savefile => 'isup_load_generator.pcap',
+               expr => 'opc == 2',
+               results => [0, 279, 0, 279, 279, 0, 279, 0, 279, 0],
+       },
+       opc_ge_2 => {
+               savefile => 'isup_load_generator.pcap',
+               expr => 'opc >= 2',
+               results => [0, 279, 0, 279, 279, 0, 279, 0, 279, 0],
+       },
+       opc_gt_2 => {
+               savefile => 'isup_load_generator.pcap',
+               expr => 'opc > 2',
+               results => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+       },
+       opc_le_2 => {
+               savefile => 'isup_load_generator.pcap',
+               expr => 'opc <= 2',
+               results => [279, 279, 279, 279, 279, 279, 279, 279, 279, 279],
+       },
+       opc_ne_0 => {
+               savefile => 'isup_load_generator.pcap',
+               expr => 'opc != 0',
+               results => [279, 279, 279, 279, 279, 279, 279, 279, 279, 279],
+       },
+       sls_eq_9 => {
+               savefile => 'isup_load_generator.pcap',
+               expr => 'sls == 9',
+               results => [279, 279, 279, 279, 279, 279, 279, 279, 279, 279],
+       },
+       sls_ne_9 => {
+               savefile => 'isup_load_generator.pcap',
+               expr => 'sls != 9',
+               results => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+       },
+);
+
 # * DLT, expr and skip: same as in accept_blocks above
 # * errstr (mandatory, string): a substring that must appear in standard error
 #   from filtertest (this verifies that the reason for rejecting the expression
@@ -5948,6 +6179,11 @@ sub accept_alias_label {
        return "${label} (alias ${index})";
 }
 
+sub apply_test_label {
+       my ($name, $type) = @_;
+       return "apply_${name}_${type}";
+}
+
 sub reject_test_label {
        my $name = shift;
        return "reject_${name}";
@@ -5961,30 +6197,8 @@ use constant {
        CHAR_TIMED_OUT => 'T',
 };
 
-sub run_accept_test {
-       my %test = @_;
-       # BSD timeout(1) does not implement --verbose.
-       my @args = defined $timeout_bin ? ($timeout_bin, $test_timeout) : ();
-       push @args, $filtertest;
-       push @args, ('-s', $test{snaplen}) if defined $test{snaplen};
-       push @args, '-O' unless $test{optimize};
-       push @args, '-l' if $test{linuxext};
-       # Write the filter expression to a file because the version of
-       # system() that takes a list does not support redirecting stdout,
-       # and the version of system() that takes a string does not escape
-       # special characters in the filter expression, which becomes
-       # invalid shell syntax.
-       file_put_contents mytmpfile ($filename_filter), $test{expr};
-       file_put_contents mytmpfile ($filename_expected), $test{expected};
-       push @args, (
-               '-F',
-               mytmpfile ($filename_filter),
-               $test{DLT},
-               '>' . mytmpfile ($filename_stdout),
-               "2>&1"
-       );
-       my $r = system (join ' ', @args) >> 8;
-
+sub validate_stdout_test {
+       my $r = system (join ' ', @_) >> 8;
        return {
                char => CHAR_TIMED_OUT,
                failure => {reason => 'filtertest timeout'}
@@ -6012,6 +6226,49 @@ sub run_accept_test {
        return {char => CHAR_PASSED};
 }
 
+sub run_accept_test {
+       my %test = @_;
+       # BSD timeout(1) does not implement --verbose.
+       my @args = defined $timeout_bin ? ($timeout_bin, $test_timeout) : ();
+       push @args, $filtertest;
+       push @args, ('-s', $test{snaplen}) if defined $test{snaplen};
+       push @args, '-O' unless $test{optimize};
+       push @args, '-l' if $test{linuxext};
+       # Write the filter expression to a file because the version of
+       # system() that takes a list does not support redirecting stdout,
+       # and the version of system() that takes a string does not escape
+       # special characters in the filter expression, which becomes
+       # invalid shell syntax.
+       file_put_contents mytmpfile ($filename_filter), $test{expr};
+       file_put_contents mytmpfile ($filename_expected), $test{expected};
+       push @args, (
+               '-F',
+               mytmpfile ($filename_filter),
+               $test{DLT},
+               '>' . mytmpfile ($filename_stdout),
+               "2>&1"
+       );
+       return validate_stdout_test @args;
+}
+
+sub run_apply_test {
+       my %test = @_;
+       my @args = defined $timeout_bin ? ($timeout_bin, $test_timeout) : ();
+       push @args, $filtertest;
+       push @args, '-O' unless $test{optimize};
+       file_put_contents mytmpfile ($filename_filter), $test{expr};
+       file_put_contents mytmpfile ($filename_expected), $test{expected};
+       push @args, (
+               '-F',
+               mytmpfile ($filename_filter),
+               '-r',
+               SAVEFILE_DIR . $test{savefile},
+               '>' . mytmpfile ($filename_stdout),
+               "2>&1"
+       );
+       return validate_stdout_test @args;
+}
+
 sub run_reject_test {
        my %test = @_;
        my @args = defined $timeout_bin ? ($timeout_bin, $test_timeout) : ();
@@ -6131,6 +6388,25 @@ foreach my $testname (sort keys %accept_blocks) {
                }
        }
 }
+foreach my $blockname (sort keys %apply_blocks) {
+       my $block = $apply_blocks{$blockname};
+       foreach ('savefile', 'expr', 'results') {
+               next if defined $block->{$_};
+               die "Internal error: apply test block '$blockname' does not define key '$_'";
+       }
+       foreach my $optunopt ('unopt', 'opt') {
+               my $lines = '';
+               $lines .= $block->{results}[$_] . "\n" for (0 .. $block->{results}->$#*);
+               push @ready_to_run, {
+                       label => apply_test_label ($blockname, $optunopt),
+                       func => \&run_apply_test,
+                       optimize => int ($optunopt eq 'opt'),
+                       expr => $block->{expr},
+                       expected => $lines,
+                       savefile => $block->{savefile},
+               };
+       }
+}
 foreach my $testname (sort keys %reject_tests) {
        my $test = $reject_tests{$testname};
        foreach ('DLT', 'expr', 'errstr') {
index be43de0e574bff1760b780719416d952a5683a05..149da84460c3063716f187eb84bc4c2df05d65f2 100644 (file)
@@ -226,13 +226,13 @@ main(int argc, char **argv)
        int gflag = 0;
 #endif
        char *infile = NULL;
+       char *insavefile = NULL;
        int Oflag = 1;
 #ifdef LINUX_BPF_EXT
        int lflag = 0;
 #endif
        int snaplen = MAXIMUM_SNAPLEN;
        char *p;
-       int dlt;
        bpf_u_int32 netmask = PCAP_NETMASK_UNKNOWN;
        char *cmdbuf;
        pcap_t *pd;
@@ -250,7 +250,7 @@ main(int argc, char **argv)
                program_name = argv[0];
 
        opterr = 0;
-       while ((op = getopt(argc, argv, "hdF:gm:Os:l")) != -1) {
+       while ((op = getopt(argc, argv, "hdF:gm:Os:lr:")) != -1) {
                switch (op) {
 
                case 'h':
@@ -273,6 +273,10 @@ main(int argc, char **argv)
                        infile = optarg;
                        break;
 
+               case 'r':
+                       insavefile = optarg;
+                       break;
+
                case 'O':
                        Oflag = 0;
                        break;
@@ -329,38 +333,54 @@ main(int argc, char **argv)
                }
        }
 
-       if (optind >= argc) {
-               usage(stderr);
-               /* NOTREACHED */
-       }
-
-       dlt = pcap_datalink_name_to_val(argv[optind]);
-       if (dlt < 0) {
-               dlt = (int)strtol(argv[optind], &p, 10);
-               if (p == argv[optind] || *p != '\0')
-                       error(EX_DATAERR, "invalid data link type %s", argv[optind]);
-       }
-
-       if (infile)
-               cmdbuf = read_infile(infile);
-       else
-               cmdbuf = copy_argv(&argv[optind+1]);
-
+       if (insavefile) {
+               if (dflag > 1)
+                       warn("-d is a no-op with -r");
 #ifdef BDEBUG
-       pcap_set_optimizer_debug(dflag);
-       pcap_set_print_dot_graph(gflag);
+               if (gflag)
+                       warn("-g is a no-op with -r");
+#endif
+#ifdef LINUX_BPF_EXT
+               if (lflag)
+                       warn("-l is a no-op with -r");
 #endif
 
-       pd = pcap_open_dead(dlt, snaplen);
-       if (pd == NULL)
-               error(EX_SOFTWARE, "Can't open fake pcap_t");
+               char errbuf[PCAP_ERRBUF_SIZE];
+               if (NULL == (pd = pcap_open_offline(insavefile, errbuf)))
+                       error(EX_NOINPUT, "Failed opening: %s", errbuf);
+       } else {
+               // Must have at least one command-line argument for the DLT.
+               if (optind >= argc) {
+                       usage(stderr);
+                       /* NOTREACHED */
+               }
+               int dlt = pcap_datalink_name_to_val(argv[optind]);
+               if (dlt < 0) {
+                       dlt = (int)strtol(argv[optind], &p, 10);
+                       if (p == argv[optind] || *p != '\0')
+                               error(EX_DATAERR, "invalid data link type %s", argv[optind]);
+               }
+               optind++;
 
+               pd = pcap_open_dead(dlt, snaplen);
+               if (pd == NULL)
+                       error(EX_SOFTWARE, "Can't open fake pcap_t");
 #ifdef LINUX_BPF_EXT
-       if (lflag) {
-               pd->bpf_codegen_flags |= BPF_SPECIAL_VLAN_HANDLING;
-               pd->bpf_codegen_flags |= BPF_SPECIAL_BASIC_HANDLING;
-       }
+               if (lflag) {
+                       pd->bpf_codegen_flags |= BPF_SPECIAL_VLAN_HANDLING;
+                       pd->bpf_codegen_flags |= BPF_SPECIAL_BASIC_HANDLING;
+               }
+#endif
+#ifdef BDEBUG
+               pcap_set_optimizer_debug(dflag);
+               pcap_set_print_dot_graph(gflag);
 #endif
+       }
+
+       if (infile)
+               cmdbuf = read_infile(infile);
+       else
+               cmdbuf = copy_argv(&argv[optind]);
 
        if (pcap_compile(pd, &fcode, cmdbuf, Oflag, netmask) < 0)
                error(EX_DATAERR, "%s", pcap_geterr(pd));
@@ -368,8 +388,8 @@ main(int argc, char **argv)
        if (!bpf_validate(fcode.bf_insns, fcode.bf_len))
                warn("Filter doesn't pass validation");
 
+       if (! insavefile) {
 #ifdef BDEBUG
-       if (cmdbuf != NULL) {
                // replace line feed with space
                for (cp = cmdbuf; *cp != '\0'; ++cp) {
                        if (*cp == '\r' || *cp == '\n') {
@@ -378,11 +398,21 @@ main(int argc, char **argv)
                }
                // only show machine code if BDEBUG defined, since dflag > 3
                printf("machine codes for filter: %s\n", cmdbuf);
-       } else
-               printf("machine codes for empty filter:\n");
 #endif
-
-       bpf_dump(&fcode, dflag);
+               bpf_dump(&fcode, dflag);
+       } else {
+               struct pcap_pkthdr *h;
+               const u_char *d;
+               int ret;
+               while (PCAP_ERROR_BREAK != (ret = pcap_next_ex(pd, &h, &d))) {
+                       if (ret == PCAP_ERROR)
+                               error(EX_IOERR, "pcap_next_ex() failed: %s", pcap_geterr(pd));
+                       if (ret == 1)
+                               printf("%d\n", pcap_offline_filter(&fcode, h, d));
+                       else
+                               error(EX_IOERR, "pcap_next_ex() failed: %d", ret);
+               }
+       }
        free(cmdbuf);
        pcap_freecode (&fcode);
        pcap_close(pd);
@@ -409,6 +439,10 @@ usage(FILE *f)
            "] [ -F file ] [ -m netmask] [ -s snaplen ] dlt [ expr ]\n",
            program_name);
        (void)fprintf(f, "       (print the filter program bytecode)\n");
+       (void)fprintf(f,
+           "  or:  %s [-O] [ -F file ] [ -m netmask] -r file [ expression ]\n",
+           program_name);
+       (void)fprintf(f, "       (print the filter program result for each packet)\n");
        (void)fprintf(f, "  or:  %s -h\n", program_name);
        (void)fprintf(f, "       (print the detailed help screen)\n");
        if (f == stdout) {
@@ -428,6 +462,7 @@ usage(FILE *f)
                (void)fprintf(f, "  -O              do not optimize the filter program\n");
                (void)fprintf(f, "  -F <file>       read the filter expression from the specified file\n");
                (void)fprintf(f, "  -s <snaplen>    set the snapshot length\n");
+               (void)fprintf(f, "  -r <file>       read the packets from this savefile\n");
                (void)fprintf(f, "\nIf no filter expression is specified, it defaults to an empty string, which\n");
                (void)fprintf(f, "accepts all packets.  If the -F option is in use, it replaces any filter\n");
                (void)fprintf(f, "expression specified as a command-line argument.\n");
diff --git a/tests/filter/decnet.pcap b/tests/filter/decnet.pcap
new file mode 100644 (file)
index 0000000..e49ffe5
Binary files /dev/null and b/tests/filter/decnet.pcap differ
diff --git a/tests/filter/isup_load_generator.pcap b/tests/filter/isup_load_generator.pcap
new file mode 100644 (file)
index 0000000..0394984
Binary files /dev/null and b/tests/filter/isup_load_generator.pcap differ
diff --git a/tests/filter/loopback.pcap b/tests/filter/loopback.pcap
new file mode 100644 (file)
index 0000000..384c0d3
Binary files /dev/null and b/tests/filter/loopback.pcap differ
diff --git a/tests/filter/pppoe.pcap b/tests/filter/pppoe.pcap
new file mode 100644 (file)
index 0000000..3296174
Binary files /dev/null and b/tests/filter/pppoe.pcap differ
diff --git a/tests/filter/pppoes.pcap b/tests/filter/pppoes.pcap
new file mode 100644 (file)
index 0000000..a2c7698
Binary files /dev/null and b/tests/filter/pppoes.pcap differ