From patchwork Sat Nov 26 15:52:17 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Steffan Karger X-Patchwork-Id: 2859 Return-Path: Delivered-To: patchwork@openvpn.net Delivered-To: patchwork@openvpn.net Received: from director14.mail.ord1d.rsapps.net ([172.27.255.53]) by backend30.mail.ord1d.rsapps.net with LMTP id SBdyLiJHgmMODAAAIUCqbw (envelope-from ) for ; Sat, 26 Nov 2022 12:04:34 -0500 Received: from proxy4.mail.iad3a.rsapps.net ([172.27.255.53]) by director14.mail.ord1d.rsapps.net with LMTP id cPYpLiJHgmM5VwAAeJ7fFg (envelope-from ) for ; Sat, 26 Nov 2022 12:04:34 -0500 Received: from smtp17.gate.iad3a ([172.27.255.53]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) by proxy4.mail.iad3a.rsapps.net with LMTPS id AHh5JyJHgmPeFgAA8Zvu4w (envelope-from ) for ; Sat, 26 Nov 2022 12:04:34 -0500 X-Spam-Threshold: 95 X-Spam-Score: 0 X-Spam-Flag: NO X-Virus-Scanned: OK X-Orig-To: openvpnslackdevel@openvpn.net X-Originating-Ip: [216.105.38.7] Authentication-Results: smtp17.gate.iad3a.rsapps.net; iprev=pass policy.iprev="216.105.38.7"; spf=pass smtp.mailfrom="openvpn-devel-bounces@lists.sourceforge.net" smtp.helo="lists.sourceforge.net"; dkim=fail (signature verification failed) header.d=sourceforge.net; dkim=fail (signature verification failed) header.d=sf.net; dkim=fail (signature verification failed) header.d=karger-me.20210112.gappssmtp.com; dmarc=fail (p=none; dis=none) header.from=karger.me X-Suspicious-Flag: YES X-Classification-ID: 6618a74e-6dac-11ed-a08b-525400723ca9-1-1 Received: from [216.105.38.7] ([216.105.38.7:53944] helo=lists.sourceforge.net) by smtp17.gate.iad3a.rsapps.net (envelope-from ) (ecelerity 4.2.38.62370 r(:)) with ESMTPS (cipher=DHE-RSA-AES256-GCM-SHA384) id 7C/01-11756-12742836; Sat, 26 Nov 2022 12:04:34 -0500 Received: from [127.0.0.1] (helo=sfs-ml-2.v29.lw.sourceforge.com) by sfs-ml-2.v29.lw.sourceforge.com with esmtp (Exim 4.95) (envelope-from ) id 1oyyaZ-0000QF-NB; Sat, 26 Nov 2022 17:03:39 +0000 Received: from [172.30.20.202] (helo=mx.sourceforge.net) by sfs-ml-2.v29.lw.sourceforge.com with esmtps (TLS1.2) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.95) (envelope-from ) id 1oyyaL-0000Pz-Mf for openvpn-devel@lists.sourceforge.net; Sat, 26 Nov 2022 17:03:25 +0000 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=sourceforge.net; s=x; h=Content-Transfer-Encoding:MIME-Version:Message-Id: Date:Subject:Cc:To:From:Sender:Reply-To:Content-Type:Content-ID: Content-Description:Resent-Date:Resent-From:Resent-Sender:Resent-To:Resent-Cc :Resent-Message-ID:In-Reply-To:References:List-Id:List-Help:List-Unsubscribe: List-Subscribe:List-Post:List-Owner:List-Archive; bh=G8NHeuRd7lrV/9tfjbFRB+5jWVzT/91jVtQe1MXAF08=; b=hka58r7td8hS/MvPDJh/5/0EeB G/hV/HbmXheS6XlzEgCeTit/lmNVEH+bf4KpdeYo0e/HOBh9Z1zDnSlcjJ8XnGGEoLhRMgEAjb2qW 2cXjd6n6WSTm2tCA0GoRvX3CjYk4y0xSqpR9ZyAB14afVpkh+3gJfNXAZintn9PmmGlE=; DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=sf.net; s=x ; h=Content-Transfer-Encoding:MIME-Version:Message-Id:Date:Subject:Cc:To:From :Sender:Reply-To:Content-Type:Content-ID:Content-Description:Resent-Date: Resent-From:Resent-Sender:Resent-To:Resent-Cc:Resent-Message-ID:In-Reply-To: References:List-Id:List-Help:List-Unsubscribe:List-Subscribe:List-Post: List-Owner:List-Archive; bh=G8NHeuRd7lrV/9tfjbFRB+5jWVzT/91jVtQe1MXAF08=; b=h 8m6c1So4mSkxEjNtIUseE4TANZ4VRm7wIvakUDBJSyoTNedvqwckxPqo26ayajpPsMRmXN1DqrtgW 9g94t+MC/uRmVI8L17SsDRmRqc2zgFN3hIMSkwT7XboGZR1dGnegvy8156Tf2t45y3twh9WRacefj 1tmkr2OiRyU5vjHY=; Received: from mail-ed1-f45.google.com ([209.85.208.45]) by sfi-mx-1.v28.lw.sourceforge.com with esmtps (TLS1.2:ECDHE-RSA-AES128-GCM-SHA256:128) (Exim 4.95) id 1oyyaD-00Ej4k-3M for openvpn-devel@lists.sourceforge.net; Sat, 26 Nov 2022 17:03:21 +0000 Received: by mail-ed1-f45.google.com with SMTP id b8so10170631edf.11 for ; Sat, 26 Nov 2022 09:03:17 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=karger-me.20210112.gappssmtp.com; s=20210112; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:from:to:cc:subject:date:message-id:reply-to; bh=G8NHeuRd7lrV/9tfjbFRB+5jWVzT/91jVtQe1MXAF08=; b=5okGZG1EqBaVBxENekPfaFxkyWzhb+hm7os4lrRUElUs4SmMxXry59BTghowjIUDkJ jPtoBO0YQ+PodKMVfWdcLVbMr81JaH3RoujDspolSdedvnt35FkyoX8LtV2U4JyyH1lW /Qgq8tdPBN/CqW0/BfsgGC9KYxDuu1E2P+bdiZZIM2uz57Kmu4wFvDPlYJM16kt18yhe G7qixidnE96qgcRfOBPn2On0PkfrF2VuliZjT2L5yghOEL6k9Rw/3jfkzuOQbentHlSn x8kmOsot8jNfAop4kbNPhm+hRg0Y8uk4+8B1ZyCTib1cDnYiizesAtdBaWPHunCxLH7D rqEw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:x-gm-message-state:from:to:cc:subject:date:message-id :reply-to; bh=G8NHeuRd7lrV/9tfjbFRB+5jWVzT/91jVtQe1MXAF08=; b=5gp8FhBeVZ6Ip1QqagJOQhdlkTHn06X9ip/uxwKL21geUCNZ0EYMoQfC2AxtFL4Y5A xLEeIoyjVxiF2yUhvlyWf+YczSFog9DEAoEs8CyoQu9ZFaRMpULcNLVv8AWaO0tQmzeG P4rQ87GyFmPifTI44wIgvW83/XrXr51LYgR2D0iBUfr9L/w6XzObdHQgIdBnveLWk52I g7rjp5b40cR7EVSy6ixflf5X3lN7xMRuy6uDxxGt6C1UsuYypi8Cl1trMuzeDChfk9BZ sOMhR865kwGaj15tIt3nIn0ud3auygXWXo/lJp6FO5KB14rWx2qeAmfeXRSmwoZ7uIkz gS0A== X-Gm-Message-State: ANoB5pkt+jmVnd6U5rySe5UMcY4luZZ/pGTXJV7JhVWHLUlU4ep3rbwd PBqP7DVPTA/tlFTfcznA33ztkeY7VZI5ww== X-Google-Smtp-Source: AA0mqf4BDrP8XF6Gm9DXyLV/+v39gtETytpDCV+Fd8+Cbvezg3VEy/WbFno7eA/3CzwzsG+OvioNag== X-Received: by 2002:aa7:c54c:0:b0:469:10c6:19d2 with SMTP id s12-20020aa7c54c000000b0046910c619d2mr25915177edr.243.1669478045432; Sat, 26 Nov 2022 07:54:05 -0800 (PST) Received: from localhost.localdomain (D96447CA.static.ziggozakelijk.nl. [217.100.71.202]) by smtp.gmail.com with ESMTPSA id gu21-20020a170906f29500b007ad86f86b4fsm2803966ejb.69.2022.11.26.07.54.04 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Sat, 26 Nov 2022 07:54:04 -0800 (PST) From: Steffan Karger To: openvpn-devel@lists.sourceforge.net Date: Sat, 26 Nov 2022 16:52:17 +0100 Message-Id: <20221126155216.25148-1-steffan@karger.me> X-Mailer: git-send-email 2.25.1 MIME-Version: 1.0 X-Spam-Report: Spam detection software, running on the system "util-spamd-2.v13.lw.sourceforge.com", has NOT identified this incoming email as spam. The original message has been attached to this so you can view it or label similar future email. If you have any questions, see the administrator of that system for details. Content preview: Add a local connection test suite, that is able to set up connections and verify in the process output that the process believes the connection was successful. This is not a replacement for the t_client test suite, but rather a fast, simple, local suite to validate .e.g control channel setup. It can replace t_cltsrv.sh (as a much faster alternative), but doe [...] Content analysis details: (-0.0 points, 6.0 required) pts rule name description ---- ---------------------- -------------------------------------------------- -0.0 RCVD_IN_DNSWL_NONE RBL: Sender listed at https://www.dnswl.org/, no trust [209.85.208.45 listed in list.dnswl.org] -0.0 RCVD_IN_MSPIKE_H2 RBL: Average reputation (+2) [209.85.208.45 listed in wl.mailspike.net] -0.0 SPF_PASS SPF: sender matches SPF record 0.0 SPF_HELO_NONE SPF: HELO does not publish an SPF Record -0.1 DKIM_VALID Message has at least one valid DKIM or DK signature 0.1 DKIM_SIGNED Message has a DKIM or DK signature, not necessarily valid X-Headers-End: 1oyyaD-00Ej4k-3M Subject: [Openvpn-devel] [RFC PATCH] Add connection test suite X-BeenThere: openvpn-devel@lists.sourceforge.net X-Mailman-Version: 2.1.21 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: openvpn-devel-bounces@lists.sourceforge.net X-getmail-retrieved-from-mailbox: Inbox Add a local connection test suite, that is able to set up connections and verify in the process output that the process believes the connection was successful. This is not a replacement for the t_client test suite, but rather a fast, simple, local suite to validate .e.g control channel setup. It can replace t_cltsrv.sh (as a much faster alternative), but does require python3 and pytest to run. Signed-off-by: Steffan Karger --- This is an RFC to get comments on whether you believe this approach is a useful addition to our current test suite. We can add many more useful tests once we agree on the approach. tests/Makefile.am | 6 +- tests/connection_tests/test_examples.py | 303 ++++++++++++++++++++++++ tests/t_connection.sh | 24 ++ 3 files changed, 331 insertions(+), 2 deletions(-) create mode 100644 tests/connection_tests/test_examples.py create mode 100755 tests/t_connection.sh diff --git a/tests/Makefile.am b/tests/Makefile.am index 89180f60..96b8565b 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -14,12 +14,14 @@ MAINTAINERCLEANFILES = \ SUBDIRS = unit_tests -test_scripts = t_client.sh t_lpback.sh t_cltsrv.sh +test_scripts = t_client.sh t_lpback.sh t_cltsrv.sh t_connection.sh if HAVE_SITNL test_scripts += t_net.sh endif -TESTS_ENVIRONMENT = top_srcdir="$(top_srcdir)" +TESTS_ENVIRONMENT = \ + top_builddir="$(top_builddir)" \ + top_srcdir="$(top_srcdir)" TESTS = $(test_scripts) dist_noinst_SCRIPTS = \ diff --git a/tests/connection_tests/test_examples.py b/tests/connection_tests/test_examples.py new file mode 100644 index 00000000..030fa21b --- /dev/null +++ b/tests/connection_tests/test_examples.py @@ -0,0 +1,303 @@ +import os +import pytest +import re +import subprocess +import tempfile +import threading +import time + +from pathlib import Path + +OPENVPN = Path(os.environ["OPENVPN_BINARY"]) +CD_PATH = Path(os.environ["WORK_DIR"]) + + +class BasicOption: + def __init__(self, name, *args): + self.name = name + self.value = " ".join(args) + + def toconfigfileitem(self): + return f"{self.name} {self.value}\n" + + +class InlineOption(BasicOption): + def __init__(self, name, *args, infile=None): + self.name = name + if infile is not None: + path = Path(infile) + if not path.is_absolute(): + path = CD_PATH / infile + self.value = open(path).read() + else: + self.value = " ".join(args) + + def toconfigfileitem(self): + return f"<{self.name}>\n{self.value.strip()}\n".strip() + + +class OpenVPNConfig: + DEFAULT_CONFIG_BASE = [ + BasicOption("dev", "null"), + BasicOption("local", "localhost"), + BasicOption("remote", "localhost"), + BasicOption("verb", "3"), + BasicOption("reneg-sec", "10"), + BasicOption("ping", "1"), + BasicOption("cd", str(CD_PATH)), + BasicOption("ca", "sample-keys/ca.crt"), + ] + DEFAULT_SERVER_PORT = "16010" + DEFAULT_CLIENT_PORT = "16011" + + def __init__(self, name="OpenVPN", options=DEFAULT_CONFIG_BASE, extra_options=[]): + self.options = options + extra_options + self.name = name + + def toconfigfile(self): + c = tempfile.NamedTemporaryFile(mode="w+") + for option in self.options: + c.write(option.toconfigfileitem() + "\n") + c.flush() + return c + + +class ServerConfig(OpenVPNConfig): + DEFAULT_SERVER_OPTIONS = [ + BasicOption("lport", OpenVPNConfig.DEFAULT_SERVER_PORT), + BasicOption("rport", OpenVPNConfig.DEFAULT_CLIENT_PORT), + BasicOption("tls-server"), + BasicOption("dh", "none"), + BasicOption("key", "sample-keys/server.key"), + BasicOption("cert", "sample-keys/server.crt"), + ] + + def __init__(self, name="Server", extra_options=[]): + super().__init__(name=name, extra_options=self.DEFAULT_SERVER_OPTIONS) + + self.options += extra_options + + +class ClientConfig(OpenVPNConfig): + DEFAULT_SERVER_OPTIONS = [ + BasicOption("lport", OpenVPNConfig.DEFAULT_CLIENT_PORT), + BasicOption("rport", OpenVPNConfig.DEFAULT_SERVER_PORT), + BasicOption("tls-client"), + BasicOption("remote-cert-tls", "server"), + BasicOption("key", "sample-keys/client.key"), + BasicOption("cert", "sample-keys/client.crt"), + ] + + def __init__(self, name="Client", extra_options=[]): + super().__init__(name=name, extra_options=self.DEFAULT_SERVER_OPTIONS) + + self.options += extra_options + + +class RegexNotFound(Exception): + def __init__(self, pattern, string): + super().__init__(f'Regex "{pattern}" does not match "{string}"') + + +class OpenVPNProcess: + def __init__(self, config, name=None): + self._configfile = config.toconfigfile() + self.name = name if name is not None else config.name + self.full_output = "" + + def __enter__(self): + self._p = subprocess.Popen( + [OPENVPN, self._configfile.name], stdout=subprocess.PIPE, text=True + ) + + def append_stdout_to_string(): + for line in self._p.stdout: + self.full_output += line + + threading.Thread(target=append_stdout_to_string).start() + + return self + + def __exit__(self, type, value, traceback): + if self._p: + self._p.terminate() + self._p.wait(timeout=1) + + print(f"{self.name} log:") + print(self.full_output) + + @property + def returncode(self): + return self._p.returncode + + def check_for_regex(self, pattern, flags=0): + if re.search(pattern, self.full_output, flags=flags) is None: + raise RegexNotFound(pattern, self.full_output) + + def wait_for_regex(self, pattern, timeout=10, re_flags=0): + compiled_regex = re.compile(pattern, re_flags) + end_time = time.time() + timeout + while compiled_regex.search(self.full_output) is None: + if time.time() > end_time: + raise RegexNotFound(pattern, self.full_output) + time.sleep(0.1) + + +def test_loopback_connection_udp(): + """Basic UDP connection setup test""" + server = OpenVPNProcess(ServerConfig()) + client = OpenVPNProcess(ClientConfig()) + + with server, client: + server.wait_for_regex("Initialization Sequence Completed") + client.wait_for_regex("Initialization Sequence Completed") + + assert server.returncode == 0 + assert client.returncode == 0 + + +def test_loopback_connection_tcp(): + """Basic TCP connection setup test""" + server = OpenVPNProcess( + ServerConfig(extra_options=[BasicOption("proto", "tcp-server")]) + ) + client = OpenVPNProcess( + ClientConfig(extra_options=[BasicOption("proto", "tcp-client")]) + ) + + with server, client: + server.wait_for_regex("Initialization Sequence Completed") + client.wait_for_regex("Initialization Sequence Completed") + + assert server.returncode == 0 + assert client.returncode == 0 + + +def test_loopback_connection_inline(): + """Basic connection setup test with inline key/cert files""" + server = OpenVPNProcess( + ServerConfig( + extra_options=[ + InlineOption("ca", infile="sample-keys/ca.crt"), + InlineOption("key", infile="sample-keys/server.key"), + InlineOption("cert", infile="sample-keys/server.crt"), + ] + ) + ) + client = OpenVPNProcess( + ClientConfig( + extra_options=[ + InlineOption("ca", infile="sample-keys/ca.crt"), + InlineOption("key", infile="sample-keys/client.key"), + InlineOption("cert", infile="sample-keys/client.crt"), + ] + ) + ) + + with server, client: + server.wait_for_regex("Initialization Sequence Completed") + client.wait_for_regex("Initialization Sequence Completed") + + assert server.returncode == 0 + assert client.returncode == 0 + + +def test_loopback_connection_tls_auth(): + """Basic connection setup test with tls-auth enabled""" + server = OpenVPNProcess( + ServerConfig(extra_options=[BasicOption("tls-auth", "sample-keys/ta.key", "0")]) + ) + client = OpenVPNProcess( + ClientConfig(extra_options=[BasicOption("tls-auth", "sample-keys/ta.key", "1")]) + ) + + with server, client: + server.wait_for_regex("Initialization Sequence Completed") + client.wait_for_regex("Initialization Sequence Completed") + + server.check_for_regex( + "Outgoing Control Channel Authentication: Using 160 bit message hash 'SHA1' for HMAC authentication" + ) + server.check_for_regex( + "Incoming Control Channel Authentication: Using 160 bit message hash 'SHA1' for HMAC authentication" + ) + client.check_for_regex( + "Outgoing Control Channel Authentication: Using 160 bit message hash 'SHA1' for HMAC authentication" + ) + client.check_for_regex( + "Incoming Control Channel Authentication: Using 160 bit message hash 'SHA1' for HMAC authentication" + ) + + assert server.returncode == 0 + assert client.returncode == 0 + + +def test_loopback_connection_tls_crypt(): + """Basic connection setup test with tls-crypt enabled""" + server = OpenVPNProcess( + ServerConfig(extra_options=[BasicOption("tls-crypt", "sample-keys/ta.key")]) + ) + client = OpenVPNProcess( + ClientConfig(extra_options=[BasicOption("tls-crypt", "sample-keys/ta.key")]) + ) + + with server, client: + server.wait_for_regex("Initialization Sequence Completed") + client.wait_for_regex("Initialization Sequence Completed") + + server.check_for_regex( + "Outgoing Control Channel Encryption: Cipher 'AES-256-CTR' initialized with 256 bit key" + ) + server.check_for_regex( + "Incoming Control Channel Encryption: Using 256 bit message hash 'SHA256' for HMAC authentication" + ) + client.check_for_regex( + "Outgoing Control Channel Encryption: Cipher 'AES-256-CTR' initialized with 256 bit key" + ) + client.check_for_regex( + "Incoming Control Channel Encryption: Using 256 bit message hash 'SHA256' for HMAC authentication" + ) + + assert server.returncode == 0 + assert client.returncode == 0 + + +def test_loopback_reneg(): + """Test that OpenVPN successfully renegotiates""" + server = OpenVPNProcess(ServerConfig(extra_options=[BasicOption("reneg-sec", "5")])) + client = OpenVPNProcess(ClientConfig()) + + with server, client: + server.wait_for_regex("Initialization Sequence Completed") + client.wait_for_regex("Initialization Sequence Completed") + + server.wait_for_regex( + "TLS: soft reset.*" + "Outgoing Data Channel: Cipher .* initialized.*" + "Incoming Data Channel: Cipher .* initialized", + re_flags=re.DOTALL, + ) + # The server initiates the renegotiation, client don't log a clear + # entry that indicates renegotiation was started, so just check that + # the data channel was initialized at least twice. + client.wait_for_regex( + "Outgoing Data Channel: Cipher .* initialized.*" + "Outgoing Data Channel: Cipher .* initialized", + re_flags=re.DOTALL, + ) + + assert server.returncode == 0 + assert client.returncode == 0 + + +@pytest.mark.xfail +def test_connection_xfail(): + """Example of a test that is marked as expected to fail + + TODO For discussion purposes only, remove before final version + """ + server = OpenVPNProcess(ServerConfig()) + with server: + server.wait_for_regex("No can do sir", timeout=1) + + assert server.returncode == 0 diff --git a/tests/t_connection.sh b/tests/t_connection.sh new file mode 100755 index 00000000..846dd4e3 --- /dev/null +++ b/tests/t_connection.sh @@ -0,0 +1,24 @@ +#!/bin/sh +set -eu + +# by changing this to 1 we can force automated builds to fail +# that are expected to have all the prerequisites +TCLIENT_SKIP_RC="${TCLIENT_SKIP_RC:-77}" +export OPENVPN_BINARY="$(readlink -f ${top_builddir}/src/openvpn/openvpn)" +export WORK_DIR="$(readlink -f ${top_srcdir}/sample/)" + +if ! which python3 > /dev/null; then + echo "$0: Python3 not found, skipping connection tests." + exit "${TCLIENT_SKIP_RC}" +fi + +if ! python3 -m pytest --version 2> /dev/null; then + echo "$0: Pytest not found, skipping connection tests." + exit "${TCLIENT_SKIP_RC}" +fi + +# TODO - possible improvements +# - Create and run from venv? +# - Integrate as separate target through Makefile.am ? +# - Use configure to create test config file ? +(cd "${top_srcdir}/tests/connection_tests" && python3 -m pytest -v)