diff mbox series

[iptables,01/12] tests: iptables-test: Implement fast test mode

Message ID 20221006002802.4917-2-phil@nwl.cc
State Changes Requested
Delegated to: Pablo Neira
Headers show
Series Speed up iptables-tests.py | expand

Commit Message

Phil Sutter Oct. 6, 2022, 12:27 a.m. UTC
Implement a faster mode of operation for suitable test files:

1) Collect all rules to add and all expected output in lists
2) Any supposedly failing rules are checked immediately like in slow
   mode.
3) Create and load iptables-restore input from the list in (1)
5) Construct the expected iptables-save output from (1) and check it in
   a single search
5) If any of the steps above fail, fall back to slow mode for
   verification and detailed error analysis. Fast mode failures are not
   fatal, merely warn about them.

To keep things simple (and feasible), avoid complicated test files
involving external commands, multiple tables or variant-specific
results.

Aside from speeding up testsuite run-time, rule searching has become
more strict since EOL char is practically part of the search string.
This revealed many false positives where the expected string was
actually a substring of the printed rule.

Signed-off-by: Phil Sutter <phil@nwl.cc>
---
 iptables-test.py | 197 ++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 196 insertions(+), 1 deletion(-)

Comments

Jan Engelhardt Oct. 6, 2022, 6:13 a.m. UTC | #1
On Thursday 2022-10-06 02:27, Phil Sutter wrote:

>+def run_test_file_fast(filename, netns):
>...
>+    elif "libarpt_" in filename:
>+        # only supported with nf_tables backend
>+        if EXECUTABLE != "xtables-nft-multi":
>+           return 0

Should this particular check for executable be part of fast_run_possible
instead? (Or somewhere else completely - if not running under x-n-m,
even slow mode is not possible ;)
Phil Sutter Oct. 6, 2022, 11:21 a.m. UTC | #2
On Thu, Oct 06, 2022 at 08:13:44AM +0200, Jan Engelhardt wrote:
> On Thursday 2022-10-06 02:27, Phil Sutter wrote:
> 
> >+def run_test_file_fast(filename, netns):
> >...
> >+    elif "libarpt_" in filename:
> >+        # only supported with nf_tables backend
> >+        if EXECUTABLE != "xtables-nft-multi":
> >+           return 0
> 
> Should this particular check for executable be part of fast_run_possible
> instead? (Or somewhere else completely - if not running under x-n-m,
> even slow mode is not possible ;)

Ah, you caught me c'n'p programming. ;)
I'll move the run_test_file_fast() call to after the same code in
run_test_file() and pass 'iptables' variable as parameter. The -save and
-restore commands may be constructed by simply appending the suffix.

Thanks, Phil
diff mbox series

Patch

diff --git a/iptables-test.py b/iptables-test.py
index b5a70e44b9e44..89220f29fe552 100755
--- a/iptables-test.py
+++ b/iptables-test.py
@@ -28,6 +28,11 @@  EBTABLES_SAVE = "ebtables-save"
 #IPTABLES_SAVE = ['xtables-save','-4']
 #IP6TABLES_SAVE = ['xtables-save','-6']
 
+IPTABLES_RESTORE = "iptables-restore"
+IP6TABLES_RESTORE = "ip6tables-restore"
+ARPTABLES_RESTORE = "arptables-restore"
+EBTABLES_RESTORE = "ebtables-restore"
+
 EXTENSIONS_PATH = "extensions"
 LOGFILE="/tmp/iptables-test.log"
 log_file = None
@@ -222,6 +227,185 @@  STDERR_IS_TTY = sys.stderr.isatty()
         return alt_res
     return res_inverse[res]
 
+def fast_run_possible(filename):
+    '''
+    Keep things simple, run only for simple test files:
+    - no external commands
+    - no multiple tables
+    - no variant-specific results
+    '''
+    table = None
+    rulecount = 0
+    for line in open(filename):
+        if line[0] in ["#", ":"] or len(line.strip()) == 0:
+            continue
+        if line[0] == "*":
+            if table or rulecount > 0:
+                return False
+            table = line.rstrip()[1:]
+        if line[0] in ["@", "%"]:
+            return False
+        if len(line.split(";")) > 3:
+            return False
+        rulecount += 1
+
+    return True
+
+def run_test_file_fast(filename, netns):
+    '''
+    Run a test file, but fast
+
+    :param filename: name of the file with the test rules
+    :param netns: network namespace to perform test run in
+    '''
+    if "libipt_" in filename:
+        iptables = IPTABLES
+        iptables_save = IPTABLES_SAVE
+        iptables_restore = IPTABLES_RESTORE
+    elif "libip6t_" in filename:
+        iptables = IP6TABLES
+        iptables_save = IP6TABLES_SAVE
+        iptables_restore = IP6TABLES_RESTORE
+    elif "libxt_"  in filename:
+        iptables = IPTABLES
+        iptables_save = IPTABLES_SAVE
+        iptables_restore = IPTABLES_RESTORE
+    elif "libarpt_" in filename:
+        # only supported with nf_tables backend
+        if EXECUTABLE != "xtables-nft-multi":
+           return 0
+        iptables = ARPTABLES
+        iptables_save = ARPTABLES_SAVE
+        iptables_restore = ARPTABLES_RESTORE
+    elif "libebt_" in filename:
+        # only supported with nf_tables backend
+        if EXECUTABLE != "xtables-nft-multi":
+           return 0
+        iptables = EBTABLES
+        iptables_save = EBTABLES_SAVE
+        iptables_restore = EBTABLES_RESTORE
+    else:
+        # default to iptables if not known prefix
+        iptables = IPTABLES
+        iptables_save = IPTABLES_SAVE
+        iptables_restore = IPTABLES_RESTORE
+
+    f = open(filename)
+
+    rules = {}
+    table = "filter"
+    chain_array = []
+    tests = 0
+
+    for lineno, line in enumerate(f):
+        if line[0] == "#" or len(line.strip()) == 0:
+            continue
+
+        if line[0] == "*":
+            table = line.rstrip()[1:]
+            continue
+
+        if line[0] == ":":
+            chain_array = line.rstrip()[1:].split(",")
+            continue
+
+        if len(chain_array) == 0:
+            return -1
+
+        tests += 1
+
+        for chain in chain_array:
+            item = line.split(";")
+            rule = chain + " " + item[0]
+
+            if item[1] == "=":
+                rule_save = chain + " " + item[0]
+            else:
+                rule_save = chain + " " + item[1]
+
+            res = item[2].rstrip()
+            if res != "OK":
+                rule = chain + " -t " + table + " " + item[0]
+                ret = run_test(iptables, rule, rule_save,
+                               res, filename, lineno + 1, netns)
+
+                if ret < 0:
+                    return -1
+                continue
+
+            if not chain in rules.keys():
+                rules[chain] = []
+            rules[chain].append((rule, rule_save))
+
+    restore_data = ["*" + table]
+    out_expect = []
+    for chain in ["PREROUTING", "INPUT", "FORWARD", "OUTPUT", "POSTROUTING"]:
+        if not chain in rules.keys():
+            continue
+        for rule in rules[chain]:
+            restore_data.append("-A " + rule[0])
+            out_expect.append("-A " + rule[1])
+    restore_data.append("COMMIT")
+
+    out_expect = "\n".join(out_expect)
+
+    # load all rules via iptables_restore
+
+    command = EXECUTABLE + " " + iptables_restore
+    if netns:
+        command = "ip netns exec " + netns + " " + command
+
+    for line in restore_data:
+        print(iptables_restore + ": " + line, file=log_file)
+
+    proc = subprocess.Popen(command, shell = True, text = True,
+                            stdin = subprocess.PIPE,
+                            stdout = subprocess.PIPE,
+                            stderr = subprocess.PIPE)
+    restore_data = "\n".join(restore_data) + "\n"
+    out, err = proc.communicate(input = restore_data)
+
+    if proc.returncode == -11:
+        reason = "iptables-restore segfaults: " + cmd
+        print_error(reason, filename, lineno)
+        return -1
+
+    if proc.returncode != 0:
+        print("%s returned %d: %s" % (iptables_restore, proc.returncode, err),
+              file=log_file)
+        return -1
+
+    # find all rules in iptables_save output
+
+    command = EXECUTABLE + " " + iptables_save
+    if netns:
+        command = "ip netns exec " + netns + " " + command
+
+    proc = subprocess.Popen(command, shell = True,
+                            stdin = subprocess.PIPE,
+                            stdout = subprocess.PIPE,
+                            stderr = subprocess.PIPE)
+    out, err = proc.communicate()
+
+    if proc.returncode == -11:
+        reason = "iptables-save segfaults: " + cmd
+        print_error(reason, filename, lineno)
+        return -1
+
+    cmd = iptables + " -F -t " + table
+    execute_cmd(cmd, filename, 0, netns)
+
+    out = out.decode('utf-8').rstrip()
+    if out.find(out_expect) < 0:
+        print("dumps differ!", file=log_file)
+        print("\n".join(["expect: " + l for l in out_expect.split("\n")]), file=log_file)
+        print("\n".join(["got: " + l
+                         for l in out.split("\n")
+                         if not l[0] in ['*', ':', '#']]),
+              file=log_file)
+        return -1
+
+    return tests
 
 def run_test_file(filename, netns):
     '''
@@ -236,6 +420,14 @@  STDERR_IS_TTY = sys.stderr.isatty()
     if not filename.endswith(".t"):
         return 0, 0
 
+    fast_failed = False
+    if fast_run_possible(filename):
+        tests = run_test_file_fast(filename, netns)
+        if tests > 0:
+            print(filename + ": " + maybe_colored('green', "OK", STDOUT_IS_TTY))
+            return tests, tests
+        fast_failed = True
+
     if "libipt_" in filename:
         iptables = IPTABLES
     elif "libip6t_" in filename:
@@ -330,7 +522,10 @@  STDERR_IS_TTY = sys.stderr.isatty()
     if netns:
         execute_cmd("ip netns del " + netns, filename)
     if total_test_passed:
-        print(filename + ": " + maybe_colored('green', "OK", STDOUT_IS_TTY))
+        suffix = ""
+        if fast_failed:
+            suffix = maybe_colored('red', " but fast mode failed!", STDOUT_IS_TTY)
+        print(filename + ": " + maybe_colored('green', "OK", STDOUT_IS_TTY) + suffix)
 
     f.close()
     return tests, passed