]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Reimplement the gnutls-cli check in Python
authorMichał Kępień <michal@isc.org>
Tue, 18 Jan 2022 10:00:46 +0000 (11:00 +0100)
committerMichał Kępień <michal@isc.org>
Tue, 18 Jan 2022 10:00:46 +0000 (11:00 +0100)
gnutls-cli is tricky to script around as it immediately closes the
server connection when its standard input is closed.  This prevents
simple shell-based I/O redirection from being used for capturing the DNS
response sent over a TLS connection and the workarounds for this issue
employ non-standard utilities like "timeout".

Instead of resorting to clever shell hacks, reimplement the relevant
check in Python.  Exit immediately upon receiving a valid DNS response
or when gnutls-cli exits in order to decrease the test's run time.
Employ dnspython to avoid the need for storing DNS queries in binary
files and to improve test readability.  Capture more diagnostic output
to facilitate troubleshooting.  Use a pytest fixture instead of an
Autoconf macro to keep test requirements localized.

bin/tests/system/conf.sh.in
bin/tests/system/doth/.gitignore [new file with mode: 0644]
bin/tests/system/doth/clean.sh
bin/tests/system/doth/conftest.py [new file with mode: 0644]
bin/tests/system/doth/example-soa-answer.good [deleted file]
bin/tests/system/doth/example-soa-request.saved [deleted file]
bin/tests/system/doth/tests.sh
bin/tests/system/doth/tests_gnutls.py [new file with mode: 0644]
configure.ac

index 54c339b8f4fa4be2370dba2cb67da95606a72491..fc5d264f98d90c401e555eac091df16614c221b7 100644 (file)
@@ -114,9 +114,6 @@ SHELL=@SHELL@
 # CURL will be empty if no program was found by configure
 CURL=@CURL@
 
-# GNUTLS_CLI will be empty if no program was found by configure
-GNUTLS_CLI=@GNUTLS_CLI@
-
 # NC will be empty if no program was found by configure
 NC=@NC@
 
diff --git a/bin/tests/system/doth/.gitignore b/bin/tests/system/doth/.gitignore
new file mode 100644 (file)
index 0000000..0cf2079
--- /dev/null
@@ -0,0 +1,4 @@
+gnutls-cli.*
+headers.*
+ns*/example.db
+ns*/named.conf
index b0915f53aa6406079f0c79053b023b713bc14984..2de4e2dcd5c6f346788fb637b67055c73aaac385 100644 (file)
@@ -20,6 +20,6 @@ rm -f ./*/named.memstats
 rm -f ./*/named.run
 rm -f ./*/named.run.prev
 rm -f ./dig.out.*
-rm -f ./example-soa-*.test*
+rm -f ./gnutls-cli.*
 rm -f ./*/example*.db
 rm -rf ./headers.*
diff --git a/bin/tests/system/doth/conftest.py b/bin/tests/system/doth/conftest.py
new file mode 100644 (file)
index 0000000..8978b65
--- /dev/null
@@ -0,0 +1,43 @@
+#!/usr/bin/python3
+
+# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+#
+# SPDX-License-Identifier: MPL-2.0
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0.  If a copy of the MPL was not distributed with this
+# file, you can obtain one at https://mozilla.org/MPL/2.0/.
+#
+# See the COPYRIGHT file distributed with this work for additional
+# information regarding copyright ownership.
+
+import os
+import shutil
+import subprocess
+
+import pytest
+
+
+@pytest.fixture
+def gnutls_cli_executable():
+    # Ensure gnutls-cli is available.
+    executable = shutil.which('gnutls-cli')
+    if not executable:
+        pytest.skip('gnutls-cli not found in PATH')
+
+    # Ensure gnutls-cli supports the --logfile command-line option.
+    args = [executable, '--logfile=/dev/null']
+    try:
+        with subprocess.check_output(args, stderr=subprocess.STDOUT) as _:
+            pass
+    except subprocess.CalledProcessError as exc:
+        stderr = exc.output
+    if b'illegal option' in stderr:
+        pytest.skip('gnutls-cli does not support the --logfile option')
+
+    return executable
+
+
+@pytest.fixture
+def named_tlsport():
+    return int(os.environ.get('TLSPORT', '853'))
diff --git a/bin/tests/system/doth/example-soa-answer.good b/bin/tests/system/doth/example-soa-answer.good
deleted file mode 100644 (file)
index d462dc6..0000000
Binary files a/bin/tests/system/doth/example-soa-answer.good and /dev/null differ
diff --git a/bin/tests/system/doth/example-soa-request.saved b/bin/tests/system/doth/example-soa-request.saved
deleted file mode 100644 (file)
index d5225b2..0000000
Binary files a/bin/tests/system/doth/example-soa-request.saved and /dev/null differ
index 5e5c9b0f0ebdd18771ad0a2065004a6cd9ebe480..119085a2539edccf6d017ff048e742975675962d 100644 (file)
@@ -582,29 +582,5 @@ if [ -n "$testcurl" ]; then
        status=$((status + ret))
 fi
 
-# check whether we can use gnutls-cli for sending test queries.
-if [ -x "${GNUTLS_CLI}" ] ; then
-       GNUTLS_CLI_CHECK="$(${GNUTLS_CLI} --logfile=/dev/null 2>&1 | grep -i 'illegal option')"
-
-       if [ -n "$GNUTLS_CLI_CHECK" ]; then
-               echo_i "The available version of gnutls-cli does not support the required features"
-       else
-               testgnutls=1
-       fi
-fi
-
-if [ -n "${testgnutls}" ] ; then
-       n=$((n + 1))
-       echo_i "checking sending a DoT query using gnutls-cli ($n)"
-       ret=0
-       # use gnutls-cli to query for 'example/SOA',
-       # use a timeout with a second empty `cat` because EOF in `stdin`
-       # causes gnutls-cli to disconnect without waiting for the answer
-       ( cat example-soa-request.saved && timeout 10 cat ) | "${GNUTLS_CLI}" --no-ca-verification --no-ocsp --alpn=dot --logfile=/dev/null --port=${TLSPORT} 10.53.0.1 > example-soa-answer.test$n 2>&1
-       diff example-soa-answer.good example-soa-answer.test$n > /dev/null 2>&1 || ret=1
-       if [ $ret != 0 ]; then echo_i "failed"; fi
-       status=$((status + ret))
-fi
-
 echo_i "exit status: $status"
 [ $status -eq 0 ] || exit 1
diff --git a/bin/tests/system/doth/tests_gnutls.py b/bin/tests/system/doth/tests_gnutls.py
new file mode 100644 (file)
index 0000000..0e7879e
--- /dev/null
@@ -0,0 +1,93 @@
+#!/usr/bin/python3
+
+# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+#
+# SPDX-License-Identifier: MPL-2.0
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0.  If a copy of the MPL was not distributed with this
+# file, you can obtain one at https://mozilla.org/MPL/2.0/.
+#
+# See the COPYRIGHT file distributed with this work for additional
+# information regarding copyright ownership.
+
+import selectors
+import struct
+import subprocess
+import time
+
+import pytest
+
+pytest.importorskip('dns')
+import dns.exception
+import dns.message
+import dns.rdatatype
+
+
+def test_gnutls_cli_query(gnutls_cli_executable, named_tlsport):
+    # Prepare the example/SOA query which will be sent over TLS.
+    query = dns.message.make_query('example.', dns.rdatatype.SOA)
+    query_wire = query.to_wire()
+    query_with_length = struct.pack('>H', len(query_wire)) + query_wire
+
+    # Run gnutls-cli.
+    gnutls_cli_args = [gnutls_cli_executable, '--no-ca-verification', '-V',
+                       '--no-ocsp', '--alpn=dot', '--logfile=gnutls-cli.log',
+                       '--port=%d' % named_tlsport, '10.53.0.1']
+    with open('gnutls-cli.err', 'wb') as gnutls_cli_stderr, \
+         subprocess.Popen(gnutls_cli_args, stdin=subprocess.PIPE,
+                          stdout=subprocess.PIPE, stderr=gnutls_cli_stderr,
+                          bufsize=0) as gnutls_cli:
+        # Send the example/SOA query to the standard input of gnutls-cli.  Do
+        # not close standard input yet because that causes gnutls-cli to close
+        # the TLS connection immediately, preventing the response from being
+        # read.
+        gnutls_cli.stdin.write(query_with_length)
+        gnutls_cli.stdin.flush()
+
+        # Keep reading data from the standard output of gnutls-cli until a full
+        # DNS message is received or a timeout is exceeded or gnutls-cli exits.
+        # Popen.communicate() cannot be used here because: a) it closes
+        # standard input after sending data to the process (see above why this
+        # is a problem), b) gnutls-cli is not DNS-aware, so it does not exit
+        # upon receiving a DNS response.
+        selector = selectors.DefaultSelector()
+        selector.register(gnutls_cli.stdout, selectors.EVENT_READ)
+        deadline = time.time() + 10
+        gnutls_cli_output = b''
+        response = b''
+        while not response and not gnutls_cli.poll():
+            if not selector.select(timeout=deadline - time.time()):
+                break
+            gnutls_cli_output += gnutls_cli.stdout.read(512)
+            try:
+                # Ignore TCP length, just try to parse a DNS message from
+                # the rest of the data received.
+                response = dns.message.from_wire(gnutls_cli_output[2:])
+            except dns.exception.FormError:
+                continue
+
+        # At this point either a DNS response was received or a timeout fired
+        # or gnutls-cli exited prematurely.  Close the standard input of
+        # gnutls-cli.  Terminate it if that does not cause it to shut down
+        # gracefully.
+        gnutls_cli.stdin.close()
+        try:
+            gnutls_cli.wait(5)
+        except subprocess.TimeoutExpired:
+            gnutls_cli.kill()
+
+    # Store the response received for diagnostic purposes.
+    with open('gnutls-cli.out.bin', 'wb') as response_bin:
+        response_bin.write(gnutls_cli_output)
+    if response:
+        with open('gnutls-cli.out.txt', 'w', encoding='utf-8') as response_txt:
+            response_txt.write(response.to_text())
+
+    # Check whether a response was received and whether it is sane.
+    assert response
+    assert query.id == response.id
+    assert len(response.answer) == 1
+    assert response.answer[0].match(dns.name.from_text('example.'),
+                                    dns.rdataclass.IN, dns.rdatatype.SOA,
+                                    dns.rdatatype.NONE)
index 4e6220d499ca0aedc294188900afd5642a06030e..6649271bb71b2d60669b1f6d7d1d020a1c01b5b4 100644 (file)
@@ -1270,13 +1270,6 @@ AC_CONFIG_FILES([doc/doxygen/doxygen-input-filter],
 AC_PATH_PROG(CURL, curl, curl)
 AC_SUBST(CURL)
 
-#
-# Look for gnutls-cli
-#
-
-AC_PATH_PROG([GNUTLS_CLI], [gnutls-cli], [])
-AC_SUBST(GNUTLS_CLI)
-
 #
 # Look for nc
 #