tests: add regression tests for xtables-translate

This test suite is intended to detect regressions in the translation
infrastructure. The script checks if ip[6]tables-translate produces the
expected output, otherwise it prints the wrong translation and the
expected one.

** Arguments

  --all     # Show also passed tests
  [test]    # Run only the specified test file

** Test files structure

Test files are located under extensions directory. Every file contains
tests about specific extension translations. A test file name must end
with ".txlate".

Inside the files, every single test is defined by two consecutive lines:
ip[6]tables-translate command and expected result. One blank line is left
between tests by convention.

e.g.

  $ cat extensions/libxt_cpu.txlate
  iptables-translate -A INPUT -p tcp --dport 80 -m cpu --cpu 0 -j ACCEPT
  nft add rule ip filter INPUT tcp dport 80 cpu 0 counter accept

  iptables-translate -A INPUT -p tcp --dport 80 -m cpu ! --cpu 1 -j ACCEPT
  nft add rule ip filter INPUT tcp dport 80 cpu != 1 counter accept

Signed-off-by: Pablo M. Bermudo Garay <pablombg@gmail.com>
Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
diff --git a/xlate-test.py b/xlate-test.py
new file mode 100755
index 0000000..006289f
--- /dev/null
+++ b/xlate-test.py
@@ -0,0 +1,85 @@
+#!/usr/bin/env python3
+# encoding: utf-8
+
+import os
+import sys
+import shlex
+import subprocess
+import argparse
+
+keywords = ("iptables-translate", "ip6tables-translate")
+
+
+if sys.stdout.isatty():
+    colors = {"magenta": "\033[95m", "green": "\033[92m", "yellow": "\033[93m",
+              "red": "\033[91m", "end": "\033[0m"}
+else:
+    colors = {"magenta": "", "green": "", "yellow": "", "red": "", "end": ""}
+
+
+def magenta(string):
+    return colors["magenta"] + string + colors["end"]
+
+
+def red(string):
+    return colors["red"] + string + colors["end"]
+
+
+def yellow(string):
+    return colors["yellow"] + string + colors["end"]
+
+
+def green(string):
+    return colors["green"] + string + colors["end"]
+
+
+def run_test(name, payload):
+    test_passed = True
+    result = []
+    result.append(yellow("## " + name.replace(".txlate", "")))
+
+    for line in payload:
+        if line.startswith(keywords):
+            output = subprocess.run(shlex.split(line), stdout=subprocess.PIPE)
+            translation = output.stdout.decode("utf-8").rstrip(" \n")
+            expected = next(payload).rstrip(" \n")
+            if translation != expected:
+                result.append(red("Fail"))
+                result.append(magenta("src: ") + line.rstrip(" \n"))
+                result.append(magenta("exp: ") + expected)
+                result.append(magenta("res: ") + translation + "\n")
+                test_passed = False
+            elif args.all:
+                result.append(green("Ok"))
+                result.append(magenta("src: ") + line.rstrip(" \n"))
+                result.append(magenta("res: ") + translation + "\n")
+
+    if not test_passed or args.all:
+        print("\n".join(result))
+
+
+def load_test_files():
+    for test in sorted(os.listdir("extensions")):
+        if test.endswith(".txlate"):
+            with open("extensions/" + test, "r") as payload:
+                run_test(test, payload)
+
+
+def main():
+    if args.test:
+        if not args.test.endswith(".txlate"):
+            args.test += ".txlate"
+        try:
+            with open("extensions/" + args.test, "r") as payload:
+                run_test(args.test, payload)
+        except IOError:
+            print(red("Error: ") + "test file does not exist")
+    else:
+        load_test_files()
+
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--all", action="store_true", help="show also passed tests")
+parser.add_argument("test", nargs="?", help="run only the specified test file")
+args = parser.parse_args()
+main()